Real data type c. Data types in C. Programming in C language

In addition to dividing data into variables and constants, there is a classification of data by type. The description of variables primarily consists of declaring their type. The data type characterizes the range of its values ​​and the form of representation in computer memory. Each type is characterized by a set of operations performed on data. Traditionally, general-purpose programming languages ​​have standard types such as integer, real, character, and logical 3 . Let us immediately note that there is no logical type in C. An expression (in the particular case, a variable) is considered true if it is non-zero, otherwise it is considered false.

The existence of two numeric types (integer and real) is associated with two possible forms of representing numbers in computer memory.

Data whole type stored in presentation form fixed point. It is characterized by absolute accuracy in representing numbers and performing operations on them, as well as a limited range of number values. The integer type is used for data that, in principle, cannot have a fractional part (number of people, cars, etc., numbers and counters).

Type real corresponds to the form of number representation floating point, which is characterized by an approximate representation of a number with a given number of significant digits (mantissa signs) and a large range of the order of the number, which makes it possible to represent both very large and very small numbers in absolute value. Due to the approximate representation of real-type data, their it is incorrect to compare for equality.

In modern implementations of universal programming languages, there are usually several integer and several real types, each of which is characterized by its size of memory allocated for one value and, accordingly, its range of number values, and for real types - and its precision (the number of digits of the mantissa).

Data character type take values ​​on the entire set of characters allowed for a given computer. One byte is allocated to store one character value; characters are encoded in accordance with a standard encoding table (usually ASCII).

There are 4 basic types in C:

char- character type;

int- a whole type,

float- single precision real type,

double- double precision real type.

To define derived types, use qualifiers:short(short) - used with type int,long(long) - used with types int And double;signed(with a sign), unsigned(unsigned) - applicable to any integer type. In the absence of the word unsigned, the value is considered signed, i.e. i.e. the default is signed. Due to the admissibility of an arbitrary combination of qualifiers and names of basic types, one type can have several designations. Information about standard C types is presented in Tables 1 and 2. Synonym descriptors are listed in the cells of the first column, separated by commas.

Table 1. Standard C integer data types

Data type

Range of values

char, signed char

unsigned int, unsigned

int, signed int, short int, short

2147483648...2147483647

Interestingly, in C, the char type can be used as a character or as an integer type, depending on the context.

Table 2. Standard C real data types

Comment. To write programs for the first part of the manual, we will mainly need two types:floatAndint.

Language Basics

The program code and the data that the program manipulates are recorded in the computer's memory as a sequence of bits. Bit is the smallest element of computer memory capable of storing either 0 or 1. At the physical level, this corresponds to electrical voltage, which, as we know, is either present or not. Looking at the contents of the computer's memory, we will see something like:
...

It is very difficult to make sense of such a sequence, but sometimes we need to manipulate such unstructured data (usually the need for this arises when programming hardware device drivers). C++ provides a set of operations for working with bit data. (We'll talk about this in Chapter 4.)
Typically, some structure is imposed on a sequence of bits, grouping the bits into bytes And words. A byte contains 8 bits, and a word contains 4 bytes, or 32 bits. However, the definition of the word may be different on different operating systems. The transition to 64-bit systems is now beginning, and more recently systems with 16-bit words have been widespread. Although the vast majority of systems have the same byte size, we will still call these quantities machine-specific.

Now we can talk about, for example, a byte at address 1040 or a word at address 1024 and say that a byte at address 1032 is not equal to a byte at address 1040.
However, we do not know what any byte, any machine word, represents. How to understand the meaning of certain 8 bits? In order to uniquely interpret the meaning of this byte (or word, or other set of bits), we must know the type of data that the byte represents.
C++ provides a set of built-in data types: character, integer, real - and a set of composite and extended types: strings, arrays, complex numbers.
In addition, for actions with this data there is a basic set of operations: comparison, arithmetic and other operations. There are also transition operators, loops, and conditional operators. These elements of the C++ language make up the set of bricks from which you can build a system of any complexity. The first step in mastering C++ will be to study the listed basic elements, which is what Part II of this book is devoted to.

Chapter 3 provides an overview of built-in and extended types, and the mechanisms by which you can create new types. This is mainly, of course, the class mechanism introduced in Section 2.3.

Chapter 4 covers expressions, built-in operations and their priorities, and type conversions. Chapter 5 talks about language instructions. Finally, Chapter 6 introduces the C++ Standard Library and the container types vector and associative array. 3. C++ data types This chapter provides an overview built-in, or elementary, data types of the C++ language. It starts with a definition literals This chapter provides an overview , such as 3.14159 or pi, and then the concept is introduced variable object, which must belong to one of the data types. The remainder of the chapter is devoted to a detailed description of each built-in type. In addition, the derived data types for strings and arrays provided by the C++ Standard Library are provided. Although these types are not elementary, they are very important for writing real C++ programs, and we want to introduce the reader to them as early as possible. We will call these data types

expansion

C++ has a set of built-in data types for representing integer and real numbers, characters, as well as a “character array” data type, which is used to store character strings. The char type is used to store individual characters and small integers. It occupies one machine byte. The types short, int and long are designed to represent integers. These types differ only in the range of values ​​that the numbers can take, and the specific sizes of the listed types are implementation dependent. Typically short takes up half a machine word, int takes one word, long takes one or two words. On 32-bit systems, int and long are usually the same size.

The float, double, and long double types are for floating-point numbers and differ in their representation precision (number of significant digits) and range.
Typically, float (single precision) takes one machine word, double (double precision) takes two, and long double (extended precision) takes three. char, short, int and long together make up whole types , which, in turn, can be iconic (signed) and unsigned

(unsigned). In signed types, the leftmost bit stores the sign (0 is plus, 1 is minus), and the remaining bits contain the value. In unsigned types, all bits are used for the value. The 8-bit signed char type can represent values ​​from -128 to 127, and the unsigned char can represent values ​​from 0 to 255. This chapter provides an overview When a certain number, for example 1, is encountered in a program, this number is called literal

literal constant

. A constant because we cannot change its value, and a literal because its value appears in the program text. A literal is an unaddressable value: although it is, of course, actually stored in the machine's memory, there is no way to know its address. Every literal has a specific type. So, 0 is of type int, 3.14159 is of type double.
Integer type literals can be written in decimal, octal, and hexadecimal. Here's what the number 20 looks like represented by decimal, octal, and hexadecimal literals:
20 // decimal

024 // octal
By default, all integer literals are of type signed int. You can explicitly define an entire literal as being of type long by appending the letter L to the end of the number (both uppercase L and lowercase l are used, but for ease of readability you should not use the lowercase: it can easily be confused with

1). The letter U (or u) at the end identifies the literal as an unsigned int, and two letters - UL or LU - as an unsigned long type. For example:

128u 1024UL 1L 8Lu

Literals representing real numbers can be written either with a decimal point or in scientific (scientific) notation. By default they are of type double. To explicitly indicate the float type, you need to use the suffix F or f, and for long double - L or l, but only if written with a decimal point. For example:

3.14159F 0/1f 12.345L 0.0 3el 1.0E-3E 2. 1.0L

The words true and false are bool literals.
Representable literal character constants are written as characters within single quotes. For example:

"a" "2" "," " " (space)

Special characters (tab, carriage return) are written as escape sequences. The following such sequences are defined (they begin with a backslash character):

New line \n horizontal tab \t backspace \b vertical tab \v carriage return \r paper feed \f call \a backslash \\ question \? single quote \" double quote \"

The general escape sequence is of the form \ooo, where ooo is one to three octal digits. This number is the character code. Using ASCII code, we can write the following literals:

\7 (bell) \14 (new line) \0 (null) \062 ("2")

A character literal can be prefixed with L (for example, L"a"), which means the special type wchar_t is a two-byte character type that is used to store characters from national alphabets if they cannot be represented by a regular char type, such as Chinese or Japanese letters.
A string literal is a string of characters enclosed in double quotes. Such a literal can span several lines; in this case, a backslash is placed at the end of the line. Special characters can be represented by their own escape sequences.

Here are examples of string literals:

In fact, a string literal is an array of character constants, where, by convention of the C and C++ languages, the last element is always a special character with code 0 (\0).
The literal "A" specifies a single character A, and the string literal "A" specifies an array of two elements: "A" and \0 (the empty character).
Since there is a type wchar_t, there are also literals of this type, denoted, as in the case of individual characters, by the prefix L:

L"a wide string literal"

A string literal of type wchar_t is a null-terminated array of characters of the same type.
If two or more string literals (such as char or wchar_t) appear in a row in a program test, the compiler concatenates them into one string. For example, the following text

"two" "some"

will produce an array of eight characters - twosome and a terminating null character. The result of concatenating strings of different types is undefined. If you write:

// this is not a good idea "two" L"some"

On one computer the result will be some meaningful string, but on another it may be something completely different. Programs that use implementation features of a particular compiler or operating system are not portable.

We strongly discourage the use of such structures.

Exercise 3.1

Explain the difference in the definitions of the following literals:

(a) "a", L"a", "a", L"a" (b) 10, 10u, 10L, 10uL, 012, 0*C (c) 3.14, 3.14f, 3.14L

Exercise 3.2

What mistakes are made in the examples below?

(a) "Who goes with F\144rgus?\014" (b) 3.14e1L (c) "two" L"some" (d) 1024f (e) 3.14UL (f) "multiple line comment"

3.2. Variables

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write:
#include
int main() (
// a first solution<< "2 raised to the power of 10: ";
// a first solution<< 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2;
// a first solution<< endl;
cout
}

return 0;
The problem was solved, although we had to repeatedly check whether the literal 2 was actually repeated 10 times. We did not make a mistake in writing this long sequence of twos, and the program produced the correct result - 1024.

But now we were asked to raise 2 to the 17th power, and then to the 23rd power. It is extremely inconvenient to modify the program text every time! And, even worse, it is very easy to make a mistake by writing an extra two or omitting it... But what if you need to print a table of powers of two from 0 to 15? Repeat two lines that have the general form 16 times:<< "2 в степени X\t"; cout << 2 * ... * 2;

Cout

Yes, we coped with the task. The customer is unlikely to delve into the details, being satisfied with the result obtained. In real life, this approach often works; moreover, it is justified: the problem is solved in a far from elegant way, but in the desired time frame. Looking for a more beautiful and competent option may turn out to be an impractical waste of time.

In this case, the “brute force method” gives the correct answer, but how unpleasant and boring it is to solve the problem in this way! We know exactly what steps need to be taken, but these steps themselves are simple and monotonous.

Involving more complex mechanisms for the same task, as a rule, significantly increases the time of the preparatory phase. In addition, the more complex mechanisms are used, the greater the likelihood of errors. But even despite the inevitable mistakes and wrong moves, the use of “high technologies” can bring benefits in development speed, not to mention the fact that these technologies significantly expand our capabilities. And – what’s interesting! – the decision process itself can become attractive.
Let's return to our example and try to “technologically improve” its implementation. We can use a named object to store the value of the power to which we want to raise our number. In addition, instead of a repeating sequence of literals, a loop operator can be used. This is what it will look like:

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write:
int main()
{
// objects of type int
int value = 2;
int pow = 10;
// a first solution<< value << " в степени "
<< pow << ": \t";
int res = 1;
// loop operator:
// repeat res calculation
// until cnt becomes greater than pow
for (int cnt=1; cnt<= pow; ++cnt)
res = res * value;
// a first solution<< res << endl;
}

value, pow, res and cnt are variables that allow you to store, modify and retrieve values. The for loop statement repeats the result line pow times.
There is no doubt that we have created a much more flexible program. However, this is still not a function. To get a real function that can be used in any program to calculate the power of a number, you need to select the general part of the calculations, and set specific values ​​​​with parameters.

Int pow(int val, int exp) ( for (int res = 1; exp > 0; --exp) res = res * val; return res; )

Now it will not be difficult to obtain any power of the desired number. Here's how our last task is implemented - print a table of powers of two from 0 to 15:

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: extern int pow(int,int); int main() ( int val = 2; int exp = 15;
cout<< "Степени 2\n";
for (int cnt=0; cnt<= exp; ++cnt)
// a first solution<< cnt << ": "
<< pow(val, cnt) << endl;
return 0;
}

Of course, our pow() function is still not general enough and not robust enough.

It cannot operate with real numbers, it incorrectly raises numbers to a negative power - it always returns 1. The result of raising a large number to a higher power may not fit into an int variable, and then some random incorrect value will be returned. Do you see how difficult it turns out to be to write functions designed for widespread use? Much more difficult than implementing a specific algorithm aimed at solving a specific problem.

3.2.1. What is a variable This chapter provides an overview Variable an object

– this is a named area of ​​​​memory that we have access to from the program; You can put values ​​there and then retrieve them. Every C++ variable has a specific type, which characterizes the size and location of that memory location, the range of values ​​it can store, and the set of operations that can be applied to that variable. Here is an example of defining five objects of different types:

Int student_count; double salary; bool on_loan; strins street_address; char delimiter; A variable, like a literal, has a specific type and stores its value in some area of ​​​​memory. Addressability

  • - that's what the literal lacks.
  • There are two quantities associated with a variable:

the actual value, or r-value (from read value - value for reading), which is stored in this memory area and is inherent in both the variable and the literal;

the address value of the memory area associated with the variable, or l-value (from location value - location value) - the place where the r-value is stored;

inherent only in the object.

In expression

The variable definition statement allocates memory for it. Since an object has only one memory location associated with it, such a statement can only appear once in a program. If a variable defined in one source file must be used in another, problems arise. For example:

// file module0.C // defines an object fileName string fileName; // ... assign fileName a value
// file module1.C
// uses the fileName object
// alas, does not compile:
// fileName is not defined in module1.C
ifstream input_file(fileName);

C++ requires that an object be known before it is first accessed. This is necessary to ensure that the object is used correctly according to its type. In our example, module1.C will cause a compilation error because the fileName variable is not defined in it. To avoid this error, we must tell the compiler about the already defined fileName variable. This is done using a variable declaration statement:

// file module1.C // uses the fileName object // fileName is declared, that is, the program receives
// information about this object without its secondary definition
extern string fileName; ifstream input_file(fileName)

A variable declaration tells the compiler that an object with a given name, of a given type, is defined somewhere in the program. Memory is not allocated for a variable when it is declared. (The extern keyword is covered in Section 8.2.)
A program can contain as many declarations of the same variable as it likes, but it can only be defined once. It is convenient to place such declarations in header files, including them in those modules that require it. This way we can store information about objects in one place and make it easy to modify if necessary.

(We'll talk more about header files in Section 8.2.)

3.2.2. Variable name Variable name, or identifier
, can consist of Latin letters, numbers and the underscore character. Uppercase and lowercase letters in names are different.

The C++ language does not limit the length of the identifier, but using too long names like gosh_this_is_an_impossibly_name_to_type is inconvenient.

Some words are keywords in C++ and cannot be used as identifiers; Table 3.1 provides a complete list of them. Table 3.1. C++ Keywords asm auto bool
break char case catch class
const const_cast continue default double
delete do dynamic_cast else enum
explicit export float extern false
goto if inline int long
mutable namespace new operator private
protected public register reinterpret_cast return
short signed sizeof static static_cast
struct switch template this throw
typedef true try typeid typename
union void union using virtual void

To make your program more understandable, we recommend following generally accepted object naming conventions:

  • the variable name is usually written in lowercase letters, for example index (for comparison: Index is the name of the type, and INDEX is a constant defined using the #define preprocessor directive);
  • the identifier must have some meaning, explaining the purpose of the object in the program, for example: birth_date or salary;

If such a name consists of several words, such as birth_date, then it is customary to either separate the words with an underscore (birth_date) or write each subsequent word with a capital letter (birthDate). It has been noticed that programmers accustomed to the Object Oriented Approach prefer to highlight words in capital letters, while those_who_have_written_a lot_in_C use the underscore character. Which of the two methods is better is a matter of taste.

3.2.3. Object Definition

In its simplest case, the object definition operator consists of type specifier And object name and ends with a semicolon. For example:

Double salary double wage; int month; int day; int year; unsigned long distance;

You can define multiple objects of the same type in one statement. In this case, their names are listed separated by commas:

Double salary, wage; int month, day, year; unsigned long distance;

Simply defining a variable does not give it an initial value. If an object is defined as global, the C++ specification guarantees that it will be initialized to zero. If the variable is local or dynamically allocated (using the new operator), its initial value is undefined, that is, it may contain some random value.
The use of such variables is a very common mistake, which is also difficult to detect. It is recommended to explicitly specify the initial value of an object, at least in cases where it is not known whether the object can initialize itself. The class mechanism introduces the concept of a default constructor, which is used to assign default values. (We already talked about this in Section 2.3. We'll talk about default constructors a little later, in Sections 3.11 and 3.15, where we'll look at the string and complex classes from the standard library.)

Int main() ( // uninitialized local object int ival;
// object of type string is initialized
// default constructor
string project;
// ...
}

The initial value can be specified directly in the variable definition statement.

In C++, two forms of variable initialization are allowed - explicit, using the assignment operator:

Int ival = 1024; string project = "Fantasia 2000";

and implicit, with the initial value specified in parentheses:

Int ival(1024); string project("Fantasia 2000");
Both options are equivalent and set the initial values ​​for the integer variable ival to 1024 and for the project string to "Fantasia 2000".

Explicit initialization can also be used when defining variables in a list:

Double salary = 9999.99, wage = salary + 0.01; int month = 08;

day = 07, year = 1955;

The variable becomes visible (and valid in the program) immediately after it is defined, so we could initialize the wage variable with the sum of the newly defined salary variable with some constant. So the definition is:
// correct, but meaningless int bizarre = bizarre;

is syntactically valid, although meaningless.

Built-in data types have a special syntax for specifying a null value:

// ival gets the value 0 and dval gets 0.0 int ival = int(); double dval = double();< int >In the following definition:

// int() is applied to each of the 10 vector elements
ivec(10);

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: Each of the ten elements of the vector is initialized with int().
(We already talked about the vector class in Section 2.8. For more on this, see Section 3.10 and Chapter 6.)
The variable can be initialized with an expression of any complexity, including function calls. For example:
#include
double price = 109.99, discount = 0.16;

double sale_price(price * discount);
string pet("wrinkles"); extern int get_value(); int val = get_value();

unsigned abs_val = abs(val);

abs() is a standard function that returns the absolute value of a parameter.

get_value() is some user-defined function that returns an integer value.

Exercise 3.3

Which of the following variable definitions contain syntax errors?

(a) int car = 1024, auto = 2048; (b) int ival = ival; (c) int ival(int()); (d) double salary = wage = 9999.99; (e) cin >> int input_value;

Exercise 3.4

(a) extern string name; string name("exercise 3.5a"); (b) extern vector

students;

vector

students;< int >Exercise 3.6

What object names are invalid in C++? Change them so that they are syntactically correct:

(a) int double = 3.14159; (b)vector

_; (c) string namespace; (d) string catch-22; (e) char 1_or_2 = "1"; (f) float Float = 3.14f;
Exercise 3.7
What is the difference between the following global and local variable definitions?
}

String global_class; int global_int; int main() (

int local_int; string local_class;// ...
3.3. Signposts

  • Pointers and dynamic memory allocation were briefly introduced in Section 2.2.
  • Pointer

is an object that contains the address of another object and allows indirect manipulation of this object. Typically, pointers are used to manipulate dynamically created objects, to construct related data structures such as linked lists and hierarchical trees, and to pass large objects—arrays and class objects—as parameters to functions.

Each pointer is associated with a certain data type, and their internal representation does not depend on the internal type: both the memory size occupied by an object of the pointer type and the range of values ​​are the same. The difference is how the compiler treats the addressed object. Pointers to different types can have the same value, but the memory area where the corresponding types are located can be different: a pointer to int containing the address value 1000 is directed to memory area 1000-1003 (on a 32-bit system); a pointer to double containing the value of address 1000 is directed to memory area 1000-1007 (on a 32-bit system).

Here are some examples:

Int *ip1, *ip2; complex

*cp; string *pstring; vector

*pvec; double *dp;

The index is indicated by an asterisk before the name. In defining variables with a list, an asterisk must precede each pointer (see above: ip1 and ip2). In the example below, lp is a pointer to a long object, and lp2 is a long object:

Long *lp, lp2;

In the following case, fp is interpreted as a float object, and fp2 is a pointer to it:
If the pointer value is 0, then it does not contain any object address.
Let a variable of type int be specified:

Int ival = 1024;

The following are examples of defining and using pointers to int pi and pi2:

//pi is initialized to zero address int *pi = 0;
// pi2 is initialized with ival address
int *pi2 =
// correct: pi and pi2 contain the address ival
pi = pi2;
// pi2 contains address zero
pi2 = 0;

A pointer cannot be assigned a value that is not an address:

// error: pi cannot accept the value int pi = ival

In the same way, you cannot assign a value to a pointer of one type that is the address of an object of another type. If the following variables are defined:

Double dval; double *ps =

then both assignment expressions below will cause a compilation error:

// compilation errors // invalid data type assignment: int*<== double* pi = pd pi = &dval;

It's not that the pi variable cannot contain the addresses of a dval object - the addresses of objects of different types have the same length. Such address mixing operations are deliberately prohibited because the compiler's interpretation of objects depends on the type of the pointer to them.
Of course, there are cases when we are interested in the value of the address itself, and not in the object to which it points (let's say we want to compare this address with some other one).

To resolve such situations, a special void pointer has been introduced, which can point to any data type, and the following expressions will be correct:

// correct: void* can contain // addresses of any type void *pv = pi; pv = pd;
The type of the object pointed to by void* is unknown and we cannot manipulate this object. All we can do with such a pointer is assign its value to another pointer or compare it with some address value. (We'll talk more about the void pointer in Section 4.14.)

In order to access an object given its address, you need to use the dereferencing operation, or indirect addressing, denoted by an asterisk (*). Having the following variable definitions:

Int ival = 1024;, ival2 = 2048; int *pi =
// indirect assignment of the variable ival to the value ival2 *pi = ival2;
// indirect use of ival variable as rvalue and lvalue
*pi = abs(*pi); // ival = abs(ival);

*pi = *pi + 1; // ival = ival + 1;
When we apply the address operator (&) to an object of type int, we get a result of type int*
If we apply the same operation to an object of type int* (pointer to int), we get a pointer to a pointer to int, i.e. int**. int** is the address of an object that contains the address of an object of type int. By dereferencing ppi, we get an object of type int* containing the address of ival. To obtain the ival object itself, the dereference operation on ppi must be applied twice.

Int **ppi = π int *pi2 = *ppi;
cout<< "Значение ival\n" << "явное значение: " << ival << "\n"
<< "косвенная адресация: " << *pi << "\n"
<< "дважды косвенная адресация: " << **ppi << "\n"

Pointers can be used in arithmetic expressions. Notice the following example, where two expressions do completely different things:

Int i, j, k; int *pi = // i = i + 2
*pi = *pi + 2; //increase the address contained in pi by 2
pi = pi + 2;

You can add an integer value to a pointer, or you can subtract from it.
Adding 1 to a pointer increases the value it contains by the size of the memory allocated to an object of the corresponding type. If char is 1 byte, int is 4 bytes, and double is 8, then adding 2 to the pointers to char, int, and double will increase their value by 2, 8, and 16, respectively. How can this be interpreted?

If objects of the same type are located one after another in memory, then increasing the pointer by 1 will cause it to point to the next object.
Therefore, pointer arithmetic is most often used when processing arrays; in any other cases they are hardly justified.
Here's what a typical example of using address arithmetic looks like when iterating over the elements of an array using an iterator:
Int ia; int *iter = int *iter_end =
}

while (iter != iter_end) (

do_something_with_value (*iter);

++iter;

Exercise 3.8

The definitions of the variables are given:

Int ival = 1024, ival2 = 2048; int *pi1 = &ival, *pi2 = &ival2, **pi3 = 0;

What happens when you perform the following assignment operations? Are there any mistakes in these examples?

(a) ival = *pi3; (e) pi1 = *pi3; (b) *pi2 = *pi3; (f) ival = *pi1; (c) ival = pi2; (g) pi1 = ival; (d) pi2 = *pi1; (h)pi3 =

Exercise 3.9

Working with pointers is one of the most important aspects of C and C++, but it's easy to make mistakes. For example, code

Pi = pi = pi + 1024;

will almost certainly cause pi to point to a random memory location. What does this assignment operator do and when will it not cause an error?
#include
Exercise 3.10
This program contains an error related to incorrect use of pointers:
cout
}

Int foobar(int *pi) ( *pi = 1024; return *pi; )

Exercise 3.11

The errors from the previous two exercises manifest themselves and lead to fatal consequences due to C++'s lack of checking for the correctness of pointer values ​​during program execution. Why do you think such a check was not implemented? Can you offer some general guidelines to make working with pointers more secure?

3.4. String types

C++ supports two types of strings - a built-in type inherited from C, and the string class from the C++ standard library. The string class provides much more capabilities and is therefore more convenient to use, however, in practice there are often situations when it is necessary to use a built-in type or have a good understanding of how it works. (One example would be parsing the command line parameters passed to the main() function. We'll cover this in Chapter 7.)

3.4.1. Built-in string type

As already mentioned, the built-in string type comes from C++, inherited from C. The character string is stored in memory as an array and accessed using a char* pointer. The C standard library provides a set of functions for manipulating strings. For example:

// returns the length of the string int strlen(const char*);
// compares two strings
int strcmp(const char*, const char*);
// copies one line to another
char* strcpy(char*, const char*);

The C Standard Library is part of the C++ library. To use it we must include the header file:

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write:

The pointer to char, which we use to access a string, points to the array of characters corresponding to the string. Even when we write a string literal like

Const char *st = "Price of a bottle of wine\n";

the compiler puts all the characters of the string into an array and then assigns st the address of the first element of the array. How can you work with a string using such a pointer?
Typically, address arithmetic is used to iterate over characters in a string. Since the string always ends with a null character, you can increment the pointer by 1 until the next character is a null. For example:

While (*st++) ( ... )

st is dereferenced and the resulting value is checked to see if it is true. Any non-zero value is considered true, and hence the loop ends when a character with code 0 is reached. The increment operation ++ adds 1 to the pointer st and thus shifts it to the next character.
This is what an implementation of a function that returns the length of a string might look like. Note that since a pointer can contain a null value (not point to anything), it should be checked before the dereference operation:

Int string_length(const char *st) ( int cnt = 0; if (st) while (*st++) ++cnt; return cnt; )

A built-in string can be considered empty in two cases: if the pointer to the string has a null value (in which case we have no string at all) or if it points to an array consisting of a single null character (that is, to a string that does not contain any significant characters).

// pc1 does not address any character array char *pc1 = 0; // pc2 addresses the null character const char *pc2 = "";

For a novice programmer, using built-in type strings is fraught with errors due to the too low level of implementation and the inability to do without address arithmetic. Below we will show some common mistakes made by beginners.

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: The task is simple: calculate the length of the string. The first version is incorrect. Fix her.
const char *st = "Price of a bottle of wine\n"; int main() (
int len ​​= 0;<< len << ": " << st;
cout
}

while (st++) ++len;
cout

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: In this version, the st pointer is not dereferenced. Therefore, it is not the character pointed to by st that is checked for equality 0, but the pointer itself. Since this pointer initially had a non-zero value (the address of the string), it will never become zero, and the loop will run endlessly.
{
const char *st = "Price of a bottle of wine\n"; int main() (
In the second version of the program this error has been eliminated. The program finishes successfully, but the output is incorrect. Where are we going wrong this time?<< len << ": " << st << endl;
cout
}

const char *st = "Price of a bottle of wine\n"; int main()
while (*st++) ++len;

cout<< len << ": " << st;

The error is that after the loop ends, the pointer st does not address the original character literal, but the character located in memory after the terminating null of that literal. Anything can be in this place, and the program's output will be a random sequence of characters.

You can try to fix this error:

St = st – len; cout

Now our program produces something meaningful, but not completely. The answer looks like this:

18: price of wine bottle

We forgot to take into account that the trailing null character was not included in the calculated length. st must be offset by the length of the string plus 1. Here is finally the correct operator:

St = st – len - 1;

Now our program produces something meaningful, but not completely. The answer looks like this:

added in order to correct a mistake made at an early stage of program design - directly increasing the st pointer. This statement does not fit into the logic of the program, and the code is now difficult to understand. These kinds of fixes are often called patches—something designed to plug a hole in an existing program.

A much better solution would be to reconsider the logic. One option in our case would be to define a second pointer initialized with the value st:

Const char *p = st;

Now p can be used in a length calculation loop, leaving the value of st unchanged:

While (*p++)

3.4.2. String class
As we just saw, using the built-in string type is error prone and inconvenient because it is implemented at such a low level. Therefore, it was quite common to develop your own class or classes to represent a string type - almost every company, department, or individual project had its own implementation of a string. What can I say, in the previous two editions of this book we did the same thing! This gave rise to problems of program compatibility and portability. The implementation of the standard string class by the C++ standard library was intended to put an end to this reinvention of bicycles.

  • Let's try to specify the minimum set of operations that the string class should have:
  • initialization with an array of characters (a built-in string) or another object of type string. A built-in type does not have the second capability;
  • copying one line to another. For a built-in type you have to use the strcpy() function;
  • access to individual characters of a string for reading and writing. In a built-in array, this is done using an index operation or indirect addressing;
  • comparing two strings for equality. For a built-in type, use the strcmp() function;
  • concatenation of two strings, producing the result either as a third string or instead of one of the original ones. For a built-in type, the strcat() function is used, but to get the result in a new line, you need to use the strcpy() and strcat() functions sequentially;
  • calculating the length of a string. You can find out the length of a built-in type string using the strlen() function;

The C++ Standard Library's string class implements all of these operations (and much more, as we'll see in Chapter 6). In this section we will learn how to use the basic operations of this class.
In order to use objects of the string class, you must include the corresponding header file:

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write:

Here is an example of a string from the previous section, represented by an object of type string and initialized to a character string:

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: string st("Price of a bottle of wine\n");

The length of the string is returned by the size() member function (the length does not include the terminating null character).

But now we were asked to raise 2 to the 17th power, and then to the 23rd power. It is extremely inconvenient to modify the program text every time! And, even worse, it is very easy to make a mistake by writing an extra two or omitting it... But what if you need to print a table of powers of two from 0 to 15? Repeat two lines that have the general form 16 times:<< "Длина " << st << ": " << st.size() << " символов, включая символ новой строки\n";

The second form of a string definition specifies an empty string:

String st2; // empty line

How do we know if a string is empty? Of course, you can compare its length with 0:

If (! st.size()) // correct: empty

However, there is also a special method empty() that returns true for an empty string and false for a non-empty one:

If (st.empty()) // correct: empty

The third form of the constructor initializes an object of type string with another object of the same type:

String st3(st);

The string st3 is initialized with the string st. How can we make sure these strings match? Let's use the comparison operator (==):

If (st == st3) // initialization worked

How to copy one line to another? Using the normal assignment operator:

St2 = st3; // copy st3 to st2

To concatenate strings, use the addition (+) or addition with assignment operation (+=). Let two lines be given:

String s1("hello, "); string s2("world\n");

We can get a third string consisting of a concatenation of the first two, this way:

String s3 = s1 + s2;

If we want to add s2 to the end of s1, we should write:

S1 += s2;

The addition operation can concatenate objects of the string class not only with each other, but also with strings of the built-in type. You can rewrite the example above so that special characters and punctuation marks are represented by a built-in type, and meaningful words are represented by objects of class string:

Const char *pc = ", "; string s1("hello"); string s2("world");
string s3 = s1 + pc + s2 + "\n";

Such expressions work because the compiler knows how to automatically convert objects of the built-in type to objects of the string class. It is also possible to simply assign a built-in string to a string object:

String s1; const char *pc = "a character array"; s1 = pc; // Right

The reverse conversion, however, does not work. Attempting to perform the following built-in type string initialization will cause a compilation error:

Char *str = s1; // compilation error

To accomplish this conversion, you must explicitly call the somewhat strangely named c_str() member function:

Char *str = s1.c_str(); // almost correct

The c_str() function returns a pointer to a character array containing the string object's string as it would appear in the built-in string type.
The above example of initializing a char *str pointer is still not entirely correct. c_str() returns a pointer to a constant array to prevent the object's contents from being directly modified by this pointer, which has type

Const char *

(We'll cover the const keyword in the next section.) The correct initialization option looks like this:

Const char *str = s1.c_str(); // Right

The individual characters of a string object, like a built-in type, can be accessed using the index operation. For example, here is a piece of code that replaces all periods with underscores:

String str("fa.disney.com"); int size = str.size(); for (int ix = 0; ix< size; ++ix) if (str[ ix ] == ".")
str[ ix ] = "_";

That's all we wanted to say about the string class right now. In fact, this class has many more interesting properties and capabilities. Let's say the previous example is also implemented by calling a single replace() function:

Replace(str.begin(), str.end(), ".", "_");

replace() is one of the general algorithms that we introduced in Section 2.8 and which will be discussed in detail in Chapter 12. This function runs through the range from begin() to end(), which return pointers to the beginning and end of the string, and replaces the elements , equal to its third parameter, to the fourth.

Exercise 3.12

Find errors in the statements below:

(a) char ch = "The long and winding road"; (b) int ival = (c) char *pc = (d) string st(&ch); (e) pc = 0; (i) pc = "0";
(f) st = pc; (j) st =
(g) ch = pc; (k) ch = *pc;
(h) pc = st; (l) *pc = ival;

Exercise 3.13

Explain the difference in behavior of the following loop statements:

While (st++) ++cnt;
while (*st++)
++cnt;

Exercise 3.14

Two semantically equivalent programs are given. The first uses the built-in string type, the second uses the string class:

// ***** Implementation using C strings ***** #include Each of the ten elements of the vector is initialized with int().
int main()
{
int errors = 0;
const char *pc = "a very long literal string";< 1000000; ++ix)
{
for (int ix = 0; ix
int len ​​= strlen(pc);
char *pc2 = new char[ len + 1 ];
strcpy(pc2, pc);
if (strcmp(pc2, pc))
}
// a first solution<< "C-строки: "
<< errors << " ошибок.\n";
++errors;
delete pc2;
int main()
{
int errors = 0;
string str("a very long literal string");< 1000000; ++ix)
{
for (int ix = 0; ix
int len ​​= str.size();
string str2 = str;
}
// a first solution<< "класс string: "
<< errors << " ошибок.\n;
}

if (str != str2)
What do these programs do?

It turns out that the second implementation runs twice as fast as the first. Did you expect such a result? How do you explain it?

Exercise 3.15

Could you improve or add anything to the set of operations of the string class given in the last section? Explain your proposals

3.5. const specifier

Let's take the following code example:< 512; ++index) ... ;

For (int index = 0; index
There are two problems with using the 512 literal. The first is the ease of perception of the program text. Why should the upper bound of the loop variable be exactly 512? What is hidden behind this value? She seems random...
The second problem concerns the ease of modification and maintenance of the code. Let's say the program has 10,000 lines and the literal 512 occurs in 4% of them. Let's say that in 80% of cases the number 512 must be changed to 1024. Can you imagine the complexity of such work and the number of mistakes that can be made by correcting the wrong value?

We solve both of these problems at the same time: we need to create an object with the value 512. Giving it a meaningful name, such as bufSize, we make the program much more understandable: it is clear what exactly the loop variable is compared to.< bufSize

Index In this case, changing the size of bufSize does not require going through 400 lines of code to modify 320 of them. How much the likelihood of errors is reduced by adding just one object! Now the value is 512.

localized< bufSize; ++index)
// ...

Int bufSize = 512; // input buffer size // ... for (int index = 0; index

One small problem remains: the bufSize variable here is an l-value that can be accidentally changed in the program, leading to a hard-to-catch error. Here's one common mistake: using the assignment operator (=) instead of the comparison operator (==):

// random change in bufSize value if (bufSize = 1) // ...
Executing this code will cause the bufSize value to be 1, which can lead to completely unpredictable program behavior. Errors of this kind are usually very difficult to detect because they are simply not visible.

Using the const specifier solves this problem. By declaring the object as

we turn the variable into a constant with value 512, the value of which cannot be changed: such attempts are suppressed by the compiler: incorrect use of the assignment operator instead of comparison, as in the example above, will cause a compilation error.

// error: attempt to assign a value to a constant if (bufSize = 0) ...

Since a constant cannot be assigned a value, it must be initialized at the place where it is defined. Defining a constant without initializing it also causes a compilation error:

Const double pi; // error: uninitialized constant

Const double minWage = 9.60; // Right? error?
double *ptr =

Should the compiler allow such an assignment? Since minWage is a constant, it cannot be assigned a value. On the other hand, nothing prevents us from writing:

*ptr += 1.40; // changing the minWage object!

As a rule, the compiler is not able to protect against the use of pointers and will not be able to signal an error if they are used in this way. This requires too deep an analysis of the program logic. Therefore, the compiler simply prohibits the assignment of constant addresses to ordinary pointers.
So, are we deprived of the ability to use pointers to constants? No. For this purpose, there are pointers declared with the const specifier:

Const double *cptr;

where cptr is a pointer to an object of type const double. The subtlety is that the pointer itself is not a constant, which means we can change its value.

For example:
Const double *pc = 0; const double minWage = 9.60; // correct: we cannot change minWage using pc
pc = double dval = 3.14; // correct: we cannot change minWage using pc
// although dval is not a constant
pc = // correct dval = 3.14159; //Right

*pc = 3.14159; // error

The address of a constant object is assigned only to a pointer to a constant. At the same time, such a pointer can also be assigned the address of a regular variable:

Pc =
In real programs, pointers to constants are most often used as formal parameters of functions. Their use ensures that the object passed to a function as an actual argument will not be modified by that function. For example:

// In real programs, pointers to constants are most often // used as formal parameters of functions int strcmp(const char *str1, const char *str2);

(We'll talk more about constant pointers in Chapter 7, when we talk about functions.)
There are also constant pointers. (Note the difference between a const pointer and a pointer to a constant!). A const pointer can address either a constant or a variable. For example:

Int errNumb = 0; int *const currErr =

Here curErr is a const pointer to a non-const object. This means that we cannot assign it the address of another object, although the object itself can be modified.

Here's how the curErr pointer could be used:
Do_something(); if (*curErr) (
errorHandler();
}

*curErr = 0; // correct: reset errNumb value

Trying to assign a value to a const pointer will cause a compilation error:

CurErr = // error

A constant pointer to a constant is a union of the two cases considered.

Const double pi = 3.14159; const double *const pi_ptr = π

Neither the value of the object pointed to by pi_ptr nor the value of the pointer itself can be changed in the program.

Exercise 3.16

Explain the meaning of the following five definitions. Are any of them wrong?

(a) int i; (d) int *const cpi; (b) const int ic; (e) const int *const cpic; (c) const int *pic;

Exercise 3.17

Which of the following definitions are correct? Why?

(a) int i = -1; (b) const int ic = i; (c) const int *pic = (d) int *const cpi = (e) const int *const cpic =

Exercise 3.18

Using the definitions from the previous exercise, identify the correct assignment operators. Explain.

(a) i = ic; (d) pic = cpic; (b) pic = (i) cpic = (c) cpi = pic; (f) ic = *cpic;

3.6. Reference type
A reference type is indicated by specifying the address operator (&) before the variable name. The link must be initialized. For example:

Int ival = 1024; // correct: refVal - reference to ival int &refVal = ival; // error: reference must be initialized to int

Int ival = 1024; // error: refVal is of type int, not int* int &refVal = int *pi = // correct: ptrVal is a reference to a pointer int *&ptrVal2 = pi;

Once a reference is defined, you can't change it to work with another object (which is why the reference must be initialized at the point where it is defined). In the following example, the assignment operator does not change the value of refVal; the new value is assigned to the variable ival - the one that refVal addresses.

Int min_val = 0; // ival receives the value of min_val, // instead of refVal changes the value to min_val refVal = min_val;

RefVal += 2; adds 2 to ival, the variable referenced by refVal. Similarly int ii = refVal; assigns ii the current value of ival, int *pi = initializes pi with the address of ival.

// two objects of type int are defined int ival = 1024, ival2 = 2048; // one reference and one object defined int &rval = ival, rval2 = ival2; // one object, one pointer and one reference are defined
int inal3 = 1024, *pi = ival3, &ri = ival3; // two links are defined int &rval3 = ival3, &rval4 = ival2;

A const reference can be initialized by an object of another type (assuming, of course, it is possible to convert one type to another), as well as by an addressless value such as a literal constant. For example:

Double dval = 3.14159; // true only for constant references
const int &ir = 1024;
const int &ir2 = dval;
const double &dr = dval + 1.0;

If we had not specified the const specifier, all three reference definitions would have caused a compilation error. However, the reason why the compiler does not pass such definitions is unclear. Let's try to figure it out.
For literals, this is more or less clear: we should not be able to indirectly change the value of a literal using pointers or references. As for objects of other types, the compiler converts the original object into some auxiliary object. For example, if we write:

Double dval = 1024; const int &ri = dval;

then the compiler converts it to something like this:

Int temp = dval; const int &ri = temp;

If we could assign a new value to the ri reference, we would actually change not dval, but temp. The dval value would remain the same, which is completely unobvious to the programmer. Therefore, the compiler prohibits such actions, and the only way to initialize a reference with an object of another type is to declare it as const.
Here is another example of a link that is difficult to understand the first time. We want to define a reference to the address of a constant object, but our first option causes a compilation error:

Const int ival = 1024; // error: constant reference needed
int *&pi_ref =

An attempt to correct the matter by adding a const specifier also fails:

Const int ival = 1024; // still an error const int *&pi_ref =

What is the reason? If we read the definition carefully, we will see that pi_ref is a reference to a constant pointer to an object of type int. And we need a non-const pointer to a constant object, so the following entry would be correct:

Const int ival = 1024; // Right
int *const &piref =

There are two main differences between a link and a pointer. First, the link must be initialized at the place where it is defined. Secondly, any change to a link transforms not the link, but the object to which it refers.

Let's look at examples. If we write:

Int *pi = 0;

we initialize the pointer pi to zero, which means that pi does not point to any object. At the same time recording
const int &ri = 0;
means something like this:
int temp = 0;

const int &ri = temp;

As for the assignment operation, in the following example:

Int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = pi = pi2;
the variable ival pointed to by pi remains unchanged, and pi receives the value of the address of the variable ival2. Both pi and pi2 now point to the same object, ival2.

If we work with links:

Int &ri = ival, &ri2 = ival2; ri = ri2;
// example of using links // The value is returned in the next_value parameter

bool get_next_value(int &next_value); // overloaded operator Matrix operator+(const Matrix&, const Matrix&);

Int ival; while (get_next_value(ival)) ...

Int &next_value = ival;

(The use of references as formal parameters of functions is discussed in more detail in Chapter 7.)

Exercise 3.19

(a) int ival = 1.01; (b) int &rval1 = 1.01; (c) int &rval2 = ival; (d) int &rval3 = (e) int *pi = (f) int &rval4 = pi; (g) int &rval5 = pi*; (h) int &*prval1 = pi; (i) const int &ival2 = 1; (j) const int &*prval2 =

Exercise 3.20

Are any of the following assignment operators erroneous (using the definitions from the previous exercise)?

(a) rval1 = 3.14159; (b) prval1 = prval2; (c) prval2 = rval1; (d) *prval2 = ival2;

Exercise 3.21

Find errors in the given instructions:

(a) int ival = 0;
const int *pi = 0;
const int &ri = 0; (b)pi =

ri =

pi =

3.7. Type bool
An object of type bool can take one of two values: true and false. For example:
// initialization of the string string search_word = get_word(); // initialization of the found variable
bool found = false; string next_word; while (cin >> next_word)
if (next_word == search_word)
found = true;
// a first solution<< "ok, мы нашли слово\n";
// ... // shorthand: if (found == true)<< "нет, наше слово не встретилось.\n";

if (found)

else cout

Although bool is one of the integer types, it cannot be declared as signed, unsigned, short or long, so the above definition is erroneous:

// error short bool found = false;
{
Objects of type bool are implicitly converted to type int. True becomes 1 and false becomes 0. For example:
Bool found = false; int occurrence_count = 0; while (/* mumble */)

found = look_for(/* something */);

// value found is converted to 0 or 1
occurrence_count += found; )

In the same way, values ​​of integer types and pointers can be converted to values ​​of type bool. In this case, 0 is interpreted as false, and everything else as true:

// returns the number of occurrences extern int find(const string&); bool found = false; if (found = find("rosebud")) // correct: found == true // returns a pointer to the element
extern int* find(int value); if (found = find(1024)) // correct: found == true

3.8. Transfers

Often you have to define a variable that takes values ​​from a certain set. Let's say a file is opened in any of three modes: for reading, for writing, for appending.

Of course, three constants can be defined to denote these modes:
Const int input = 1; const int output = 2; const int append = 3;

and use these constants:
Using an enum type solves this problem. When we write:

Enum open_modes( input = 1, output, append );

we define a new type open_modes. Valid values ​​for an object of this type are limited to the set of 1, 2, and 3, with each of the specified values ​​having a mnemonic name. We can use the name of this new type to define both an object of that type and the type of the function's formal parameters:

Void open_file(string file_name, open_modes om);

input, output and append are enumeration elements. The set of enumeration elements specifies the allowed set of values ​​for an object of a given type.

A variable of type open_modes (in our example) is initialized with one of these values; it can also be assigned any of them. For example:

Open_file("Phoenix and the Crane", append);

An attempt to assign a value other than one of the enumeration elements to a variable of this type (or pass it as a parameter to a function) will cause a compilation error. Even if we try to pass an integer value corresponding to one of the enumeration elements, we will still receive an error:

// error: 1 is not an element of the enumeration open_modes open_file("Jonah", 1);

There is a way to define a variable of type open_modes, assign it the value of one of the enumeration elements and pass it as a parameter to the function:

Open_modes om = input;

But now we were asked to raise 2 to the 17th power, and then to the 23rd power. It is extremely inconvenient to modify the program text every time! And, even worse, it is very easy to make a mistake by writing an extra two or omitting it... But what if you need to print a table of powers of two from 0 to 15? Repeat two lines that have the general form 16 times:<< input << " " << om << endl;

// ... om = append;

open_file("TailTell", om);

But now we were asked to raise 2 to the 17th power, and then to the 23rd power. It is extremely inconvenient to modify the program text every time! And, even worse, it is very easy to make a mistake by writing an extra two or omitting it... But what if you need to print a table of powers of two from 0 to 15? Repeat two lines that have the general form 16 times:<< open_modes_table[ input ] << " " << open_modes_table[ om ] << endl Будет выведено: input append

However, it is not possible to obtain the names of such elements. If we write the output statement:

then we still get:

This problem is solved by defining a string array in which the element with an index equal to the value of the enumeration element will contain its name.

Given such an array, we can write:

Integer values ​​corresponding to different elements of the same enumeration do not have to be different. For example:

// point2d == 2, point2w == 3, point3d == 3, point3w == 4 enum Points ( point2d=2, point2w, point3d=3, point3w=4 );

An object whose type is an enumeration can be defined, used in expressions, and passed to a function as an argument. Such an object is initialized with only the value of one of the enumeration elements, and only that value is assigned to it, either explicitly or as the value of another object of the same type. Even integer values ​​corresponding to valid enumeration elements cannot be assigned to it:

Void mumble() ( Points pt3d = point3d; // correct: pt2d == 3 // error: pt3w is initialized with type int Points pt3w = 3; // error: polygon is not included in the Points enumeration pt3w = polygon; // correct: both object of type Points pt3w = pt3d )

However, in arithmetic expressions, an enumeration can be automatically converted to type int. For example:

Const int array_size = 1024; // correct: pt2w is converted to int
int chunk_size = array_size * pt2w;

3.9. Type "array"

We already touched on arrays in section 2.1. An array is a set of elements of the same type, accessed by an index - the serial number of the element in the array. For example:

Int ival;

defines ival as an int variable, and the instruction

Int ia[ 10 ];

specifies an array of ten objects of type int. To each of these objects, or array elements, can be accessed using the index operation:

Ival = ia[ 2 ];

assigns the variable ival the value of array element ia with index 2. Similarly

Ia[ 7 ] = ival;

assigns the element at index 7 the value ival.

An array definition consists of a type specifier, an array name, and a size.

The size specifies the number of array elements (at least 1) and is enclosed in square brackets. The size of the array must be known already at the compilation stage, and therefore it must be a constant expression, although it is not necessarily specified as a literal.
Here are examples of correct and incorrect array definitions:
Extern int get_size(); // buf_size and max_files constants

The buf_size and max_files objects are constants, so the input_buffer and fileTable array definitions are correct. But staff_size is a variable (albeit initialized with the constant 27), which means salaries are unacceptable. (The compiler is unable to find the value of the staff_size variable when the salaries array is defined.)
The max_files-3 expression can be evaluated at compile time, so the fileTable array definition is syntactically correct.
Element numbering starts at 0, so for an array of 10 elements the correct index range is not 1 – 10, but 0 – 9. Here is an example of iterating through all the elements of the array:

Int main() ( const int array_size = 10; int ia[ array_size ]; for (int ix = 0; ix< array_size; ++ ix)
ia[ ix ] = ix;
}

When defining an array, you can explicitly initialize it by listing the values ​​of its elements in curly braces, separated by commas:

Const int array_size = 3; int ia[ array_size ] = ( 0, 1, 2 );

If we explicitly specify a list of values, then we don’t have to specify the size of the array: the compiler itself will count the number of elements:

// array of size 3 int ia = ( 0, 1, 2 );

When both the size and the list of values ​​are explicitly specified, three options are possible. If the size and number of values ​​coincide, everything is obvious. If the list of values ​​is shorter than the specified size, the remaining elements of the array are initialized to zero.

If there are more values ​​in the list, the compiler displays an error message:

// ia ==> ( 0, 1, 2, 0, 0 ) const int array_size = 5; int ia[ array_size ] = ( 0, 1, 2 );

A character array can be initialized not only with a list of character values ​​in curly braces, but also with a string literal. However, there are some differences between these methods. Let's say

Const char cal = ("C", "+", "+" ); const char cal2 = "C++";

The dimension of the ca1 array is 3, the ca2 array is 4 (in string literals, the terminating null character is taken into account). The following definition will cause a compilation error:

// error: the string "Daniel" consists of 7 elements const char ch3[ 6 ] = "Daniel";

An array cannot be assigned the value of another array, and initialization of one array by another is not allowed. Additionally, it is not allowed to use an array of references.
{
Here are examples of the correct and incorrect use of arrays:
Const int array_size = 3; int ix, jx, kx; // correct: array of pointers of type int* int *iar = ( &ix, &jx, &kx ); // error: reference arrays are not allowed int &iar = ( ix, jx, kx ); int main()
ia3 = ia;
cout
}

To copy one array to another, you have to do this for each element separately:

Const int array_size = 7; int ia1 = ( 0, 1, 2, 3, 4, 5, 6 ); int main() (
int ia3[ array_size ];< array_size; ++ix)
for (int ix = 0; ix
}

ia2[ ix ] = ia1[ ix ];

return 0;

An array index can be any expression that produces a result of an integer type. For example:

Int someVal, get_index(); ia2[ get_index() ] = someVal;

We emphasize that the C++ language does not provide control of array indices, either at the compilation stage or at the runtime stage. The programmer himself must ensure that the index does not go beyond the boundaries of the array. Errors when working with an index are quite common. Unfortunately, it is not very difficult to find examples of programs that compile and even work, but nevertheless contain fatal errors that sooner or later lead to crash.

Exercise 3.22

Which of the following array definitions are incorrect? Explain.

(a) int ia[ buf_size ]; (d) int ia[ 2 * 7 - 14 ] (b) int ia[ get_size() ]; (e) char st[ 11 ] = "fundamental"; (c) int ia[ 4 * 7 - 14 ];

Exercise 3.23<= array_size; ++ix)
The following code snippet must initialize each element of the array with an index value. Find the mistakes made:
}

Int main() ( const int array_size = 10; int ia[ array_size ]; for (int ix = 1; ix

ia[ ia ] = ix;

// ...

3.9.1. Multidimensional arrays

In C++, it is possible to use multidimensional arrays, when declaring them you must indicate the right boundary of each dimension in separate square brackets.

Here is the definition of a two-dimensional array:

Int ia[ 4 ][ 3 ];

The first value (4) specifies the number of rows, the second (3) – the number of columns.

The ia object is defined as an array of four strings of three elements each. Multidimensional arrays can also be initialized:

Int ia[ 4 ][ 3 ] = ( ( 0, 1, 2 ), ( 3, 4, 5 ), ( 6, 7, 8 ), ( 9, 10, 11 ) );

Int ia[ 4 ][ 3 ] = ( 0, 3, 6, 9 );

When accessing elements of a multidimensional array, you must use indexes for each dimension (they are enclosed in square brackets). This is what initializing a two-dimensional array looks like using nested loops:

Int main() ( const int rowSize = 4; const int colSize = 3; int ia[ rowSize ][ colSize ]; for (int = 0; i< rowSize; ++i)
for (int j = 0; j< colSize; ++j)
ia[ i ][ j ] = i + j j;
}

Design

Ia[ 1, 2 ]

is valid from the point of view of C++ syntax, but it does not mean at all what an inexperienced programmer expects. This is not a declaration of a two-dimensional 1-by-2 array. An aggregate in square brackets is a comma-separated list of expressions that will result in the final value 2 (see the comma operator in Section 4.2). Therefore the declaration ia is equivalent to ia.

This is another opportunity to make a mistake.

3.9.2. Relationship between arrays and pointers

If we have an array definition:

Int ia = ( 0, 1, 1, 2, 3, 5, 8, 13, 21 );

then what does simply indicating his name in the program mean?

Using an array identifier in a program is equivalent to specifying the address of its first element:

Similarly, you can access the value of the first element of an array in two ways:

// both expressions return the first element *ia; ia;

To take the address of the second element of the array, we must write:

As we mentioned earlier, the expression

also gives the address of the second element of the array. Accordingly, its meaning is given to us in the following two ways:

*(ia+1); ia;

Note the difference in expressions:

*ia+1 and *(ia+1); The dereference operation has a higher a priority

than the addition operation (operator priorities are discussed in Section 4.13).

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: Therefore, the first expression first dereferences the variable ia and gets the first element of the array, and then adds 1 to it. The second expression delivers the value of the second element.<< *pbegin <<; ++pbegin; } }

The pbegin pointer is initialized to the address of the first element of the array. Each pass through the loop increments this pointer by 1, which means it is shifted to the next element. How do you know where to stay? In our example, we defined a second pend pointer and initialized it with the address following the last element of the ia array. As soon as the value of pbegin equals pend, we know that the array has ended. Let's rewrite this program so that the beginning and end of the array are passed as parameters to a certain generalized function that can print an array of any size:

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: void ia_print(int *pbegin, int *pend) (
while (pbegin != pend) (
// a first solution<< *pbegin << " ";
++pbegin;
}
) int main()
{
int ia = ( 0, 1, 1, 2, 3, 5, 8, 13, 21 );
ia_print(ia, ia + 9);
}

Our function has become more universal, however, it can only work with arrays of type int. There is a way to remove this limitation: convert this function into a template (templates were briefly introduced in section 2.5):

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: template void print(elemType *pbegin, elemType *pend) ( while (pbegin != pend) ( cout<< *pbegin << " "; ++pbegin; } }

Now we can call our print() function to print arrays of any type:

Int main() ( int ia = ( 0, 1, 1, 2, 3, 5, 8, 13, 21 ); double da = ( 3.14, 6.28, 12.56, 25.12 ); string sa = ( "piglet", " eeyore", "pooh" ); print(ia, ia+9);
print(da, da+4);
print(sa, sa+3);
}

We wrote generalized function. The standard library provides a set of generic algorithms (we already mentioned this in Section 3.4) implemented in a similar way. The parameters of such functions are pointers to the beginning and end of the array with which they perform certain actions. Here, for example, is what calls to a generalized sorting algorithm look like:

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: int main() ( int ia = ( 107, 28, 3, 47, 104, 76 ); string sa = ( "piglet", "eeyore", "pooh" ); sort(ia, ia+6);
sort(sa, sa+3);
};

(We'll go into more detail about generalized algorithms in Chapter 12; the Appendix will give examples of their use.)
The C++ Standard Library contains a set of classes that encapsulate the use of containers and pointers. (This was discussed in Section 2.8.) In the next section, we'll take a look at the standard container type vector, which is an object-oriented implementation of an array.

3.10. Vector class

Using the vector class (see Section 2.8) is an alternative to using built-in arrays. This class provides much more functionality, so its use is preferable. However, there are situations when you cannot do without arrays of the built-in type. One of these situations is the processing of command line parameters passed to the program, which we will discuss in section 7.8. The vector class, like the string class, is part of the C++ standard library.
To use the vector you must include the header file:

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write:

There are two completely different approaches to using a vector, let's call them the array idiom and the STL idiom. In the first case, a vector object is used in exactly the same way as an array of a built-in type. A vector of a given dimension is determined:

Vector< int >In the following definition:

which is similar to defining an array of a built-in type:

Int ia[ 10 ];

To access individual elements of a vector, the index operation is used:

Void simp1e_examp1e() ( const int e1em_size = 10; vector< int >ivec(e1em_size);< e1em_size; ++ix)
int ia[ e1em_size ];
}

for (int ix = 0; ix

ia[ ix ] = ivec[ ix ]; // ...< ivec.size(); ++ix)
// a first solution<< ivec[ ix ] << " ";
}

We can find out the dimension of a vector using the size() function and check if the vector is empty using the empty() function. For example:

Vector< int >Void print_vector(vector

ivec) ( if (ivec.empty()) return; for (int ix=0; ix
The elements of the vector are initialized with default values. For numeric types and pointers, this value is 0. If the elements are class objects, then the initiator for them is specified by the default constructor (see section 2.3). However, the initiator can also be specified explicitly using the form:

ivec(10, -1);

All ten elements of the vector will be equal to -1.

An array of a built-in type can be explicitly initialized with a list:< int >Int ia[ 6 ] = ( -2, -1, O, 1, 2, 1024 );

A similar action is not possible for an object of the vector class. However, such an object can be initialized using a built-in array type:

// 6 ia elements are copied to ivec vector< int >ivec(ia, ia+6);

The constructor of the vector ivec is passed two pointers - a pointer to the beginning of the array ia and to the element following the last one. It is permissible to specify not the entire array, but a certain range of it, as a list of initial values:

Vector< string >svec; void init_and_assign() ( // one vector is initialized by another vector< string >user_names(svec);
// ... // one vector is copied to another
}

svec = user_names;

Vector< string >When we talk about the STL idiom, we mean a completely different approach to using a vector. Instead of immediately specifying the desired size, we define an empty vector:

text;

Then we add elements to it using various functions. For example, the push_back() function inserts an element at the end of a vector. Here's a piece of code that reads a sequence of lines from standard input and adds them to a vector:

String word;

But now we were asked to raise 2 to the 17th power, and then to the 23rd power. It is extremely inconvenient to modify the program text every time! And, even worse, it is very easy to make a mistake by writing an extra two or omitting it... But what if you need to print a table of powers of two from 0 to 15? Repeat two lines that have the general form 16 times:<< "считаны слова: \n"; for (int ix =0; ix < text.size(); ++ix) cout << text[ ix ] << " "; cout << endl;

while (cin >> word) ( text.push_back(word); // ... )

But now we were asked to raise 2 to the 17th power, and then to the 23rd power. It is extremely inconvenient to modify the program text every time! And, even worse, it is very easy to make a mistake by writing an extra two or omitting it... But what if you need to print a table of powers of two from 0 to 15? Repeat two lines that have the general form 16 times:<< "считаны слова: \n"; for (vectorAlthough we can use the index operation to iterate over the elements of a vector:<< *it << " "; cout << endl;

A more typical use of iterators within this idiom would be:
::iterator it = text.begin(); it != text.end(); ++it) cout

An iterator is a standard library class that is essentially a pointer to an array element.

Expression

Vector dereferences the iterator and gives the vector element itself. Instructions

Moves the pointer to the next element. There is no need to mix these two approaches.

If you follow the STL idiom when defining an empty vector:

ivec;

Vector It would be a mistake to write:

We don't have a single element of the ivec vector yet; The number of elements is determined using the size() function.

The opposite mistake can also be made. If we defined a vector of some size, for example:< int >ia(10);< size; ++ix) ivec.push_back(ia[ ix ]);

Then inserting elements increases its size, adding new elements to existing ones.
Although this seems obvious, a novice programmer might well write:

Const int size = 7; int ia[ size ] = ( 0, 1, 1, 2, 3, 5, 8 ); vector

ivec(size); for (int ix = 0; ix
This meant initializing the ivec vector with the values ​​of the ia elements, which instead resulted in an ivec vector of size 14.

Following the STL idiom, you can not only add but also remove elements from a vector.< vector< int >(We will look at all this in detail and with examples in Chapter 6.)
Exercise 3.24< int >Are there any errors in the following definitions?
int ia[ 7 ] = ( 0, 1, 1, 2, 3, 5, 8 );< int >(a)vector
>ivec;< string >(b)vector
ivec = ( 0, 1, 1, 2, 3, 5, 8 );< string >(c)vector

ivec(ia, ia+7);

(d)vector
svec = ivec;
(e)vector svec(10, string("null"));
The is_equal() function compares two containers element by element. In the case of containers of different sizes, the “tail” of the longer one is not taken into account. It is clear that if all compared elements are equal, the function returns true, if at least one is different, false. Use an iterator to iterate over elements. Write a main() function that calls is_equal().

3.11. class complex

The class of complex numbers complex is another class from the standard library.

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write:

As usual, to use it you need to include the header file:

A complex number consists of two parts - real and imaginary. The imaginary part is the square root of a negative number. A complex number is usually written in the form

where 2 is the real part, and 3i is the imaginary part. Here are examples of definitions of objects of type complex:< double >// purely imaginary number: 0 + 7-i complex< float >purei(0, 7); // imaginary part is 0: 3 + Oi complex< long double >rea1_num(3); // both real and imaginary parts are equal to 0: 0 + 0-i complex< double >zero; // initialization of one complex number with another complex

purei2(purei);

Since complex, like vector, is a template, we can instantiate it with the types float, double and long double, as in the examples given. You can also define an array of elements of type complex:< double >Complex< double >conjugate[ 2 ] = ( complex< double >(2, -3) };

Since complex, like vector, is a template, we can instantiate it with the types float, double and long double, as in the examples given. You can also define an array of elements of type complex:< double >(2, 3), complex< double >*ptr = complex

&ref = *ptr;

3.12. typedef directive

The typedef directive allows you to specify a synonym for a built-in or custom data type. For example: Typedef double wages; typedef vector

vec_int; typedef vec_int test_scores; typedef bool in_attendance; typedef int *Pint;

Names defined using the typedef directive can be used in exactly the same way as type specifiers: // double hourly, weekly; wages hourly, weekly; //vector
vecl(10); vec_int vecl(10); //vector< bool >test0(c1ass_size); const int c1ass_size = 34; test_scores test0(c1ass_size); //vector< in_attendance >attendance; vector

attendance(c1ass_size); // int *table[ 10 ]; Pint table [10];
What are names defined using the typedef directive used for? By using mnemonic names for data types, you can make your program easier to understand. In addition, it is common to use such names for complex composite types that are otherwise difficult to understand (see the example in Section 3.14), to declare pointers to functions and member functions of a class (see Section 13.6).
Below is an example of a question that almost everyone answers incorrectly. The error is caused by a misunderstanding of the typedef directive as a simple text macro substitution.

Definition given:

Typedef char *cstring;

What is the type of the cstr variable in the following declaration:

Extern const cstring cstr;

The answer that seems obvious is:

Const char *cstr

However, this is not true. The const specifier refers to cstr, so the correct answer is a const pointer to char:

Char *const cstr;

3.13. volatile specifier
An object is declared volatile if its value can be changed without the compiler noticing, such as a variable being updated by the system clock. This specifier tells the compiler that it does not need to optimize the code to work with this object.

The volatile specifier is used similarly to the const specifier:

Volatile int disp1ay_register; volatile Task *curr_task; volatile int ixa[ max_size ]; volatile Screen bitmap_buf;
display_register is a volatile object of type int. curr_task – pointer to a volatile object of the Task class. ixa is an unstable array of integers, and each element of such an array is considered unstable. bitmap_buf is a volatile object of the Screen class, each of its data members is also considered volatile.

The only purpose of using the volatile specifier is to tell the compiler that it cannot determine who can change the value of a given object and how.

Therefore, the compiler should not perform optimizations on code that uses this object.

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write:

3.14. Class pair

The pair class of the C++ standard library allows us to define a pair of values ​​with one object if there is any semantic connection between them.< string, string >These values ​​can be the same or different types. To use this class you must include the header file:

For example, instructions
Pair

author("James", "Joyce");
Joyce.second == "Joyce")
firstBook = "Stephen Hero";

If you need to define several objects of the same type of this class, it is convenient to use the typedef directive:

Typedef pair< string, string >Authors; Authors proust("marcel", "proust"); Authors joyce("James", "Joyce"); Authors musil("robert", "musi1");

Here is another example of using a pair. The first value contains the name of some object, the second is a pointer to the table element corresponding to this object.

Class EntrySlot; extern EntrySlot* 1ook_up(string); typedef pair< string, EntrySlot* >SymbolEntry; SymbolEntry current_entry("author", 1ook_up("author"));
// ... if (EntrySlot *it = 1ook_up("editor")) (
current_entry.first = "editor";
current_entry.second = it;
}

(We'll return to the pair class when we talk about container types in Chapter 6 and about generic algorithms in Chapter 12.)

3.15. Class Types

The class mechanism allows you to create new data types; with its help, the string, vector, complex and pair types discussed above were introduced. In Chapter 2, we introduced the concepts and mechanisms that support object and object-oriented approaches, using the example of the implementation of the Array class. Here, based on the object approach, we will create a simple String class, the implementation of which will help us understand, in particular, operator overloading - we talked about it in section 2.3. (Classes are covered in detail in Chapters 13, 14, and 15.) We have given a brief description of the class in order to give more interesting examples. A reader new to C++ may want to skip this section and wait for a more systematic description of classes in later chapters.)
Our String class must support initialization by an object of the String class, a string literal, and the built-in string type, as well as the operation of assigning values ​​to these types. We use class constructors and an overloaded assignment operator for this. Access to individual String characters will be implemented as an overloaded index operation. In addition, we will need: the size() function to obtain information about the length of the string; the operation of comparing objects of type String and a String object with a string of a built-in type; as well as the I/O operations of our object. Finally, we implement the ability to access the internal representation of our string as a built-in string type.
A class definition begins with the keyword class, followed by an identifier—the name of the class, or type. In general, a class consists of sections preceded by the words public (open) and private (closed). A public section typically contains a set of operations supported by the class, called methods or member functions of the class. These member functions define the public interface of the class, in other words, the set of actions that can be performed on objects of that class. A private section typically includes data members that provide internal implementation. In our case, the internal members include _string - a pointer to a char, as well as _size of type int. _size will store information about the length of the string, and _string will be a dynamically allocated array of characters. This is what a class definition looks like:

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: class String; istream& operator>>(istream&, String&);
ostream& operator<<(ostream&, const String&); class String {
public:
// set of constructors
// for automatic initialization
// String strl; // String()
// String str2("literal"); // String(const char*);
// String str3(str2); // String(const String&);
String();
String(const char*);
String(const String&);
// destructor
~String();
// assignment operators
// strl = str2
// str3 = "a string literal" String& operator=(const String&);
String& operator=(const char*);
// equality testing operators
// strl == str2;
// str3 == "a string literal";
bool operator==(const String&);
bool operator==(const char*);
}

// overloading the access operator by index

// strl[ 0 ] = str2[ 0 ];

char& operator(int);
// access to class members

int size() ( return _size; )

char* c_str() ( return _string; ) private:

int_size;

char *_string;

The String class has three constructors. As discussed in Section 2.3, the overloading mechanism allows you to define multiple implementations of functions with the same name, as long as they all differ in the number and/or types of their parameters.

First constructor

It is the default constructor because it does not require an explicit initial value. When we write:
For str1, such a constructor is called.

The two remaining constructors each have one parameter. Yes, for

This will cause a compilation error, because there is no constructor with a parameter of type int.
An overloaded operator declaration has the following format:

Return_type operator op(parameter_list);

Where operator is a keyword, and op is one of the predefined operators: +, =, ==, and so on. (See Chapter 15 for a precise definition of the syntax.) Here is the declaration of the overloaded index operator:

Char& operator(int);

This operator has a single parameter of type int and returns a reference to a char.
An overloaded operator may itself be overloaded if the parameter lists of individual instantiations differ. For our String class, we will create two different assignment and equality operators.

To call a member function, use the member access operators dot (.) or arrow (->). Let us have declarations of objects of type String:
String object("Danny");
String *ptr = new String("Anna");
String array;
Here's what a call to size() looks like on these objects: vector

sizes(3);
// access member for objects (.); // objects has a size of 5 sizes[ 0 ] = object.size(); // member access for pointers (->)
// ptr has size 4
sizes[ 1 ] = ptr->size(); // access member (.)
// array has size 0

sizes[ 2 ] = array.size();
It returns 5, 4 and 0 respectively.

Overloaded operators are applied to an object in the same way as regular ones:
String name("Yadie"); String name2("Yodie"); // bool operator==(const String&)
if (name == name2)
return;
else
// String& operator=(const String&)

name = name2;

A member function declaration must be inside a class definition, and a function definition can be inside or outside a class definition. (Both the size() and c_str() functions are defined inside a class.) If a function is defined outside a class, then we must specify, among other things, which class it belongs to.
In this case, the definition of the function is placed in the source file, say String.C, and the definition of the class itself is placed in the header file (String.h in our example), which must be included in the source:
delete pc2;
// contents of the source file: String.C // enabling the definition of the String class
#include "String.h" // include the definition of the strcmp() function
bool // return type
String:: // class to which the function belongs
{
operator== // function name: equality operator
(const String &rhs) // list of parameters
if (_size != rhs._size)
return false;
}

Recall that strcmp() is a C standard library function. It compares two built-in strings, returning 0 if the strings are equal and non-zero if they are not equal. The conditional operator (?:) tests the value before the question mark. If true, the value of the expression to the left of the colon is returned; otherwise, the value to the right is returned. In our example, the value of the expression is false if strcmp() returned a non-zero value, and true if it returned a zero value. (The conditional operator is discussed in section 4.7.)
The comparison operation is used quite often, the function that implements it turned out to be small, so it is useful to declare this function built-in (inline). The compiler substitutes the text of the function instead of calling it, so no time is wasted on such a call. (Built-in functions are discussed in Section 7.6.) A member function defined within a class is built-in by default. If it is defined outside the class, in order to declare it built-in, you need to use the inline keyword:

Inline bool String::operator==(const String &rhs) ( // the same thing )

The definition of a built-in function must be in the header file that contains the class definition. By redefining the == operator as an inline operator, we must move the function text itself from the String.C file to the String.h file.
The following is the implementation of the operation of comparing a String object with a built-in string type:

Inline bool String::operator==(const char *s) ( return strcmp(_string, s) ? false: true; )

The constructor name is the same as the class name. It is considered not to return a value, so there is no need to specify a return value either in its definition or in its body. There can be several constructors. Like any other function, they can be declared inline.

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: // default constructor inline String::String()
{
_size = 0;
_string = 0;
) inline String::String(const char *str) ( if (! str) ( _size = 0; _string = 0; ) else ( _size = str1en(str); strcpy(_string, str); ) // copy constructor
inline String::String(const String &rhs)
{
size = rhs._size;
if (! rhs._string)
_string = 0;
else(
_string = new char[ _size + 1 ];
} }

Since we dynamically allocated memory using the new operator, we need to free it by calling delete when we no longer need the String object. Another special member function serves this purpose - the destructor, which is automatically called on an object at the moment when this object ceases to exist. (See Chapter 7 about object lifetime.) The name of a destructor is formed from the tilde character (~) and the name of the class. Here is the definition of the String class destructor. This is where we call the delete operation to free the memory allocated in the constructor:

Inline String: :~String() ( delete _string; )

Both overloaded assignment operators use the special keyword this.
When we write:

String namel("orville"), name2("wilbur");
name = "Orville Wright";
this is a pointer that addresses the name1 object within the function body of the assignment operation.
this always points to the class object through which the function is called.
If
ptr->size();

obj[ 1024 ];

Then inside size() the value of this will be the address stored in ptr. Inside the index operation, this contains the address of obj. By dereferencing this (using *this), we get the object itself. (The this pointer is described in detail in Section 13.4.)

Inline String& String::operator=(const char *s) ( if (! s) ( _size = 0; delete _string; _string = 0; ) else ( _size = str1en(s); delete _string; _string = new char[ _size + 1 ]; strcpy(_string, s); return *this;

When implementing an assignment operation, one mistake that is often made is that they forget to check whether the object being copied is the same one into which the copy is being made. We will perform this check using the same this pointer:

Inline String& String::operator=(const String &rhs) ( // in the expression // namel = *pointer_to_string // this represents name1, // rhs - *pointer_to_string. if (this != &rhs) (

Here is the complete text of the operation of assigning an object of the same type to a String object:
_string = 0;
else(
_string = new char[ _size + 1 ];
Inline String& String::operator=(const String &rhs) ( if (this != &rhs) ( delete _string; _size = rhs._size; if (! rhs._string)
}
}
strcpy(_string, rhs._string);
}

return *this;

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: The operation of taking an index is almost identical to its implementation for the Array array that we created in section 2.3:
inline char&
{
String::operator (int elem)< _size);
assert(elem >= 0 && elem
}

Input and output operators are implemented as separate functions rather than class members.

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: (We'll talk about the reasons for this in Section 15.2. Sections 20.4 and 20.5 talk about overloading the iostream library's input and output operators.) Our input operator can read a maximum of 4095 characters. setw() is a predefined manipulator, it reads a given number of characters minus 1 from the input stream, thereby ensuring that we do not overflow our internal buffer inBuf. (Chapter 20 discusses the setw() manipulator in detail.) To use manipulators, you must include the corresponding header file:

inline istream& operator>>(istream &io, String &s) ( // artificial limit: 4096 characters const int 1imit_string_size = 4096; char inBuf[ limit_string_size ]; // setw() is included in the iostream library // it limits the size of the readable block to 1imit_string_size -l io >> setw(1imit_string_size) >> inBuf; s = mBuf; // String::operator=(const char*);<< не является функцией-членом, он не имеет доступа к закрытому члену данных _string. Ситуацию можно разрешить двумя способами: объявить operator<< дружественным классу String, используя ключевое слово friend (дружественные отношения рассматриваются в разделе 15.2), или реализовать встраиваемую (inline) функцию для доступа к этому члену. В нашем случае уже есть такая функция: c_str() обеспечивает доступ к внутреннему представлению строки. Воспользуемся ею при реализации операции вывода:

The output operator needs access to the internal representation of the String.<<(ostream& os, const String &s) { return os << s.c_str(); }

Since operator

Let's imagine that we are solving the problem of raising 2 to the power of 10. We write: Inline ostream& operator
Below is an example program using the String class. This program takes words from the input stream and counts their total number, as well as the number of "the" and "it" words, and records the vowels encountered.
#include "String.h" int main() ( int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, theCnt = 0, itCnt = 0, wdCnt = 0, notVowel = 0; / / The words "The" and "It"
// we will check using operator==(const char*)
String but, the("the"), it("it");<<(ostream&, const String&)
// a first solution<< buf << " "; if (wdCnt % 12 == 0)
// a first solution<< endl; // String::operator==(const String&) and
// operator>>(ostream&, String&)
while (cin >> buf) (
++wdCnt;
// operator
// String::operator==(const char*);
if (buf == the | | buf == "The")
++theCnt;< buf.sizeO; ++ix)
{
else
if (buf == it || buf == "It")
{
++itCnt;
// invokes String::s-ize()
for (int ix =0; ix
// invokes String::operator(int)
switch(buf[ ix ])
case "a": case "A": ++aCnt; break;
}
}
case "e": case "E": ++eCnt; break;<<(ostream&, const String&)
// a first solution<< "\n\n"
<< "Слов: " << wdCnt << "\n\n"
<< "the/The: " << theCnt << "\n"
<< "it/It: " << itCnt << "\n\n"
<< "согласных: " < << "a: " << aCnt << "\n"
<< "e: " << eCnt << "\n"
<< "i: " << ICnt << "\n"
<< "o: " << oCnt << "\n"
<< "u: " << uCnt << endl;
}

case "i": case "I": ++iCnt; break;

Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, 1ike a fiery bird in flight. A beautiful fiery bird, he tells her, magical but untamed. "Daddy, shush, there is no such thing," she tells him, at the same time wanting him to tell her more. Shyly, she asks, "I mean, Daddy, is there?" Words: 65
the/The: 2
it/It: 1
consonants: 190
a: 22
e: 30
i: 24
o: 10
u: 7

Exercise 3.26

Our implementations of constructors and assignment operators contain a lot of repetition. Consider moving duplicate code into a separate private member function, as was done in Section 2.3. Make sure the new option works.

Exercise 3.27

Modify the test program so that it also counts the consonants b, d, f, s, t.

Exercise 3.28

Write a member function that counts the number of occurrences of a character in a String using the following declaration:

Class String ( public: // ... int count(char ch) const; // ... );

Exercise 3.29

Implement the string concatenation operator (+) so that it concatenates two strings and returns the result in a new String object. Here is the function declaration:

Class String ( public: // ... String operator+(const String &rhs) const; // ... );

This section will discuss the main data types in C++; these data types are also called built-in. The C++ programming language is an extensible programming language. The term extensible means that in addition to the built-in data types, you can create your own data types. That's why there are a huge number of data types in C++. We will study only the main ones.

Table 1 - C++ data types
Type byte Range of accepted values

integer (Boolean) data type

asm 1 0 / 255

integer (character) data type

char 1 0 / 255

integer data types

short int 2 -32 768 / 32 767
unsigned short int 2 0 / 65 535
int 4
unsigned int 4 0 / 4 294 967 295
long int 4 -2 147 483 648 / 2 147 483 647
unsigned long int 4 0 / 4 294 967 295

floating point data types

float 4 -2 147 483 648.0 / 2 147 483 647.0
long float 8
double 8 -9 223 372 036 854 775 808 .0 / 9 223 372 036 854 775 807.0

Table 1 presents the main data types in C++. The entire table is divided into three columns. The first column indicates a reserved word, which will determine, each its own, data type. The second column indicates the number of bytes allocated for a variable with the corresponding data type. The third column shows the range of acceptable values. Please note that in the table all data types are arranged from smallest to largest.

bool data type

The first one in the table is the bool data type an integer data type, since the range of valid values ​​is integers from 0 to 255. But as you have already noticed, in parentheses it says logical data type, and this is also true. Because bool used exclusively to store the results of Boolean expressions. A Boolean expression can have one of two results: true or false. true - if the logical expression is true, false - if the logical expression is false.

But since the range of valid values ​​of the bool data type is from 0 to 255, it was necessary to somehow match this range with the logical constants true and false defined in the programming language. Thus, the constant true is equivalent to all numbers from 1 to 255 inclusive, while the constant false is equivalent to only one integer - 0. Consider a program using the bool data type.

// data_type.cpp: Defines the entry point for the console application. #include "stdafx.h" #include using namespace std; int main(int argc, char* argv) ( bool boolean = 25; // variable of type bool named boolean if (boolean) // condition of the if cout operator<< "true = " << boolean << endl; // выполнится в случае истинности условия else cout << "false = " << boolean << endl; // выполнится в случае, если условие ложно system("pause"); return 0; }

IN line 9type variable declared bool , which is initialized to 25. Theoretically, afterlines 9, in a boolean variable should contain the number 25, but in fact this variable contains the number 1. As I said, the number 0 is a false value, the number 1 is a true value. The point is that in a variable like bool can contain two values ​​- 0 (false) or 1 (true). Whereas under the data type bool a whole byte is allocated, which means that a variable of type bool can contain numbers from 0 to 255. To determine false and true values, only two values ​​0 and 1 are needed. The question arises: “What are the other 253 values ​​for?”

Based on this situation, we agreed to use the numbers from 2 to 255 as the equivalent of the number 1, that is, truth. This is precisely why the boolean variable contains the number 25 and not 1. In lines 10 -13 declared, which transfers control to the operator in line 11, if the condition is true, and the operator in line 13, if the condition is false. The result of the program is shown in Figure 1.

True = 1 To continue, press any key. . .

Figure 1 - bool data type

Data type char

The char data type is an integer data type that is used to represent characters. That is, each character corresponds to a certain number from the range. The char data type is also called the character data type, since the graphical representation of characters in C++ is possible thanks to char. To represent characters in C++, the char data type is allocated one byte, one byte contains 8 bits, then we raise two to the power of 8 and get the value 256 - the number of characters that can be encoded. Thus, using the char data type, you can display any of the 256 characters. All encoded characters are represented in .

ASCII (from English Standard Code for Information Interchange) - American standard code for information exchange.

Consider a program using the char data type.

// symbols.cpp: Defines the entry point for the console application. #include "stdafx.h" #include using namespace std; int main(int argc, char* argv) ( char symbol = "a"; // declaring a variable of type char and initializing it with the symbol "a" cout<< "symbol = " << symbol << endl; // печать символа, содержащегося в переменной symbol char string = "сайт"; // объявление символьного массива (строки) cout << "string = " << string << endl; // печать строки system("pause"); return 0; }

So, in line 9a variable named symbol , it is assigned the symbol value"a" ( ASCII code). IN line 10 cout operator prints the character contained in the variable symbol IN line 11declared a string array with the name string , and the size of the array is specified implicitly. A string is stored in a string array"website" . Please note that when we saved the symbol into a variable like char , then after the equal sign we put single quotes in which we wrote the symbol. When initializing a string array with a certain string, double quotes are placed after the equal sign, in which a certain string is written. Like a regular character, strings are output using the operator cout , line 12. The result of the program is shown in Figure 2.

Symbol = a string = site To continue, press any key. . .

Figure 2 - char data type

Integer Data Types

Integer data types are used to represent numbers. There are six of them in Table 1: short int, unsigned short int, int, unsigned int, long int, unsigned long int . They all have their own memory size and range of accepted values. Depending on the compiler, the size of memory occupied and the range of accepted values ​​may vary. In Table 1, all ranges of accepted values ​​and sizes of occupied memory are taken for the MVS2010 compiler. Moreover, all data types in Table 1 are arranged in increasing order of the size of the occupied memory and the range of accepted values. The range of accepted values, one way or another, depends on the size of the occupied memory. Accordingly, the larger the size of the occupied memory, the larger the range of accepted values. Also, the range of accepted values ​​changes if the data type is declared with the unsigned prefix. The unsigned prefix means that the data type cannot store signed values, then the range of positive values ​​is doubled, for example, the short int and unsigned short int data types.

Integer data type prefixes:

short the prefix shortens the data type to which it is applied by reducing the size of the memory it occupies;

long the prefix extends the data type to which it is applied by increasing the size of the memory it occupies;

unsigned—the prefix doubles the range of positive values, while the range of negative values ​​cannot be stored in this data type.

So, essentially, we have one integer type to represent integers: the int data type. Thanks to the prefixes short, long, unsigned, a certain variety of int data types appears, differing in the size of the memory occupied and (or) the range of accepted values.

Floating Point Data Types

There are two types of floating point data in C++: float and double. Floating point data types are designed to store floating point numbers. The float and double data types can store both positive and negative floating point numbers. The float data type has half the memory footprint of the double data type, which means the range of accepted values ​​is also smaller. If the float data type is declared with the long prefix, then the range of accepted values ​​will be equal to the range of accepted values ​​of the double data type. Basically, floating point data types are needed to solve problems with high computational precision, such as money transactions.

So, we have looked at the main points regarding the main data types in C++. All that remains is to show where all these ranges of accepted values ​​and the sizes of occupied memory come from. And for this we will develop a program that will calculate the main characteristics of all the types of data discussed above.

// data_types.cpp: Defines the entry point for the console application. #include "stdafx.h" #include // I/O manipulation library #include // header file of mathematical functions #include using namespace std; int main(int argc, char* argv) ( cout<< " data type " << "byte" << " " << " max value "<< endl // column headers <<"bool = " << sizeof(bool) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных bool*/ << (pow(2,sizeof(bool) * 8.0) - 1) << endl << "char = " << sizeof(char) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных char*/ << (pow(2,sizeof(char) * 8.0) - 1) << endl << "short int = " << sizeof(short int) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных short int*/ << (pow(2,sizeof(short int) * 8.0 - 1) - 1) << endl << "unsigned short int = " << sizeof(unsigned short int) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных unsigned short int*/ << (pow(2,sizeof(unsigned short int) * 8.0) - 1) << endl << "int = " << sizeof(int) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных int*/ << (pow(2,sizeof(int) * 8.0 - 1) - 1) << endl << "unsigned int = " << sizeof(unsigned int) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных unsigned int*/ << (pow(2,sizeof(unsigned int) * 8.0) - 1) << endl << "long int = " << sizeof(long int) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных long int*/ << (pow(2,sizeof(long int) * 8.0 - 1) - 1) << endl << "unsigned long int = " << sizeof(unsigned long int) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных undigned long int*/ << (pow(2,sizeof(unsigned long int) * 8.0) - 1) << endl << "float = " << sizeof(float) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных float*/ << (pow(2,sizeof(float) * 8.0 - 1) - 1) << endl << "double = " << sizeof(double) << " " << fixed << setprecision(2) /*вычисляем максимальное значение для типа данных double*/ << (pow(2,sizeof(double) * 8.0 - 1) - 1) << endl; system("pause"); return 0; }

This program is posted so that you can view the characteristics of data types in your system. There is no need to understand the code, since the program uses control statements that you most likely are not yet familiar with. For a superficial acquaintance with the program code, I will explain some points below. Operator sizeof() Calculates the number of bytes allocated for a data type or variable. Function pow(x,y) elevates meaning x to the power of y , this function is available from the header file . fixed and setprecision() manipulators available from header file . The first one is fixed , passes values ​​in fixed form to the output stream. Manipulator setprecision(n) displays n decimal places. The maximum value of a certain data type is calculated using the following formula:

Max_val_type = 2^(b * 8 - 1) - 1; // for data types with negative and positive numbers // where b is the number of bytes allocated in memory for a variable with this data type // multiply by 8, since there are 8 bits in one byte // subtract 1 in parentheses, since the range numbers must be divided in two for positive and negative values ​​// subtract 1 at the end, since the range of numbers starts from zero // data types with the prefix unsigned max_val_type = 2^(b * 8) - 1; // for data types only with positive numbers // the explanations for the formula are similar, only the unit is not subtracted from the bracket

An example of how the program works can be seen in Figure 3. The first column shows the main data types in C++, the second column shows the size of memory allocated for each data type, and the third column shows the maximum value that the corresponding data type can contain. The minimum value is found similar to the maximum. For data types with the unsigned prefix, the minimum value is 0.

Data type byte max value bool = 1 255.00 char = 1 255.00 short int = 2 32767.00 unsigned short int = 2 65535.00 int = 4 2147483647.00 unsigned int = 4 4294967295.00 long int = 4 2147483647.0 0 unsigned long int = 4 4294967295.00 float = 4 2147483647.00 double = 8 9223372036854775808.00 To continue, press any key. . .

Figure 3 - C++ data types

If, for example, a variable of type short int is assigned the value 33000, then the bit grid will overflow, since the maximum value in a variable of type short int is 32767. That is, some other value will be stored in a variable of type short int, most likely it will be negative. Since we've touched on the int data type, it's worth noting that you can omit the int keyword and write, for example, just short . The compiler will interpret such an entry as short int . The same applies to the prefixes long and unsigned. For example:

// shorthand for data type int short a1; // same as short int long a1; // same as long int unsigned a1; // same as unsigned int unsigned short a1; // same as unsigned short int

Data types in C are a class of data whose values ​​have similar characteristics. The type defines the internal representation of data in memory. The most basic data types: logical, integer, floating point numbers, strings, pointers.

With dynamic typing, a variable is associated with a type at the time of initialization. It turns out that a variable in different parts of the code can have different types. Dynamic typing is supported by Java Script, Python, Ruby, PHP.

Static typing is the opposite of dynamic typing. When declared, a variable receives a type that does not change later. The C and C++ languages ​​are just that. This method is the most convenient for writing complex code, and many errors are eliminated at the compilation stage.

Languages ​​are informally divided into strongly typed and weakly typed. Strong typing means that the compiler will throw an error if the expected and actual types do not match.

x = 1 + “2”; //error - you cannot add a symbol to a number

An example of weak typing.

Type consistency checking is carried out by the type safety system. A typing error occurs, for example, when trying to use a number as a function. There are untyped languages. In contrast to typed ones, they allow you to perform any operation on each object.

Memory classes

Variables, regardless of their type, have their own scope and lifetime.

Memory classes:

  • auto;
  • static;
  • extern;
  • register.

All variables in the C language are local by default. They can only be used within a function or block. When the function completes, their value is destroyed.

A static variable is also local, but can have a different value outside its block, and the value is retained between function calls.

The external variable is global. It is available in any part of the code and even in another file.

Data type specifiers in C may not be specified in the following cases:

  1. All variables inside the block are not variables; therefore, if this particular memory class is intended to be used, then the auto specifier is not specified.
  2. All functions declared outside a block or function are global by default, so the extern specifier is not required.

To indicate simple types, use the int, char, float, or double specifiers. The modifiers unsigned, signed, short, long, long long can be substituted for variables.

By default, all numbers are signed; therefore, they can only be in the range of positive numbers. To define a variable of type char as signed, write signed char. Long, long long, and short indicate how much memory space is allocated for storage. The largest is long long, the smallest is short.

Char is the smallest data type in C. Only 1 byte of memory is allocated to store values. A variable of type character is usually assigned characters, and less often - numbers. Character values ​​are enclosed in quotes.

The int type stores integers, its size is undefined - it takes up to 4 bytes of memory, depending on the computer architecture.

An explicit conversion of an unsigned variable is specified as follows:

The implicit looks like this:

Float and double define numbers with a dot. Float numbers are represented as -2.3 or 3.34. Double is used for greater precision - more digits are indicated after the integer and fractional separator. This type takes up more memory space than float.

Void has an empty value. It defines functions that return nothing. This specifier specifies an empty value in method arguments. Pointers, which can accept any data type, are also defined as void.

Boolean type Bool

Used in condition tests and loops. Has only two meanings:

  • true;
  • lie.

Boolean values ​​can be converted to an int value. True is equivalent to one, false is equivalent to zero. Type conversion is only possible between bool and int, otherwise the compiler will throw an error.

if (x) ( //Error: "Cannot implicitly convert type 'int' to 'bool""

if (x != 0) // The C# way

Strings and Arrays

Arrays are complex data types in C. PL does not work with strings in the same way as Javascript or Ruby do. In C, all strings are arrays of elements with a character value. Lines end with a null byte “

An important difference between the SI language and other languages ​​(PL1, FORTRAN, etc.) is the absence of a default principle, which leads to the need to declare all variables used in the program explicitly along with an indication of their corresponding types.

Variable declarations have the following format:

[memory-class-specifier] type-specifier descriptor [=initiator] [,descriptor [= initiator] ]...

A descriptor is an identifier for a simple variable or a more complex construct with square brackets, parentheses, or an asterisk (a set of asterisks).

A type specifier is one or more keywords that determine the type of the variable being declared. The SI language has a standard set of data types, using which you can construct new (unique) data types.

Initiator - specifies the initial value or list of initial values ​​that are assigned to the variable when declared.

Memory class specifier - determined by one of the four SI keywords: auto, extern, register, static, and indicates how memory will be allocated for the declared variable, on the one hand, and, on the other, the scope of this variable, i.e. , from which parts of the program can it be accessed.

1.2.1 Data type categories

Keywords to define basic data types

Integer types: Float types: char float int double short long double long signed unsigned

A variable of any type can be declared unmodifiable. This is achieved by adding the const keyword to the type-specifier. Objects with type const represent read-only data, i.e. this variable cannot be assigned a new value. Note that if there is no type-specifier after the word const, then the type-specifier int is implied. If the keyword const comes before the declaration of composite types (array, structure, mixture, enumeration), then this leads to the fact that each element must also be unmodifiable, i.e. it can only be assigned a value once.

Const double A=2.128E-2;

const B=286; (const int B=286 is implied)

Examples of declaring composite data will be discussed below.

1.2.2. Integer data type

To define data of an integer type, various keywords are used, which determine the range of values ​​and the size of the memory area allocated for variables (Table 6).

Table 6

Note that the signed and unsigned keywords are optional. They indicate how the zero bit of the declared variable is interpreted, i.e., if the unsigned keyword is specified, then the zero bit is interpreted as part of a number, otherwise the zero bit is interpreted as signed. If the unsigned keyword is missing, the integer variable is considered signed. If the type specifier consists of a key type signed or unsigned followed by a variable identifier, then it will be treated as a variable of type int. For example:

Unsigned int n;

The following note should be made: the SI language does not define the memory representation and range of values ​​for identifiers with the int and unsigned int type modifiers. The memory size for a variable with the signed int type modifier is determined by the length of the machine word, which has a different size on different machines. So, on 16-bit machines the word size is 2 bytes, on 32-bit machines it is 4 bytes, i.e. the int type is equivalent to the short int or long int types, depending on the architecture of the PC used. Thus, the same program can work correctly on one computer and incorrectly on another. To determine the length of the memory occupied by a variable, you can use the SI language sizeof operator, which returns the length of the specified type modifier.

For example:

A = sizeof(int);

b = sizeof(long int);

For example:

c = sizeof(unsigned long);

d = sizeof(short);

Note also that octal and hexadecimal constants can also have the unsigned modifier. This is achieved by specifying the prefix u or U after the constant; a constant without this prefix is ​​considered signed.

0xA8C (int signed);

01786l (long signed);

0xF7u (int unsigned);

1.2.3. Floating data

A pointer is the address of memory allocated to accommodate an identifier (the identifier can be the name of a variable, array, structure, or string literal). If a variable is declared as a pointer, then it contains a memory address where a scalar value of any type can be located. When declaring a variable of type pointer, you must specify the type of data object whose address the variable will contain, and the name of the pointer preceded by an asterisk (or group of asterisks). Pointer declaration format:

type-specifier [ modifier ] * specifier.

The type-specifier specifies the type of the object and can be any basic type, structure type, mixture (this will be discussed below). By specifying the keyword void instead of the type specifier, you can in a unique way defer the specification of the type to which the pointer refers. A variable declared as a pointer to type void can be used to refer to an object of any type. However, in order to be able to perform arithmetic and logical operations on pointers or on the objects to which they point, it is necessary to explicitly determine the type of objects when performing each operation. Such type definitions can be made using the type casting operation.

The keywords const, near, far, huge can be used as modifiers when declaring a pointer. The const keyword indicates that the pointer cannot be modified in the program. The size of a variable declared as a pointer depends on the computer architecture and on the memory model used for which the program will be compiled. Pointers to different data types do not have to be the same length.

To modify the size of the pointer, you can use the keywords near, far, huge.

Unsigned int * a; /* variable a is a pointer to type unsigned int */ double * x; /* variable x indicates double precision floating point data type */ char * fuffer ; /* declare a pointer named fuffer which points to a variable of type char */ double nomer;

void *addresses;

addresses = &nomer;

(double *)addres++;

/* The addres variable is declared as a pointer to an object of any type.

Therefore, it can be assigned the address of any object (& is the operation of calculating the address). However, as noted above, no arithmetic operation can be performed on a pointer unless the type of data to which it points is explicitly determined. This can be done by using a cast operation (double *) to convert addres to a pointer to type double, and then incrementing the address. */ const * dr;

/* The dr variable is declared as a pointer to a constant expression, i.e. The value of a pointer can change during program execution, but the value it points to cannot. */ unsigned char * const w = &obj.

/* The variable w is declared as a constant pointer to unsigned char data.

This means that w will point to the same memory location throughout the program. The contents of this area may be changed. */

In the first format 1, enum names and values ​​are specified in an enumeration list. The optional enumeration-tag-name is an identifier that names the enumeration tag defined by the enumeration list. The descriptor names an enumeration variable. More than one enumeration type variable can be specified in a declaration.

An enumeration list contains one or more constructs of the form:

identifier [= constant expression]

Each identifier names an element of the enumeration. All identifiers in the enum list must be unique. If there is no constant expression, the first identifier corresponds to the value 0, the next identifier to the value 1, etc. The name of an enumeration constant is equivalent to its value.

An identifier associated with a constant expression takes on the value specified by that constant expression. The constant expression must be of type int and can be either positive or negative. The next identifier in the list is assigned the value of the constant expression plus 1 if that identifier does not have its own constant expression. The use of enumeration elements must obey the following rules:

1. The variable may contain duplicate values.

2. Identifiers in an enumeration list must be distinct from all other identifiers in the same scope, including regular variable names and identifiers from other enumeration lists.

3. Names of enum types must be distinct from other names of enum types, structures, and mixtures in the same scope.

4. The value can follow the last element of the enumeration list.

Enum week ( SUB = 0, /* 0 */ VOS = 0, /* 0 */ POND, /* 1 */ VTOR, /* 2 */ SRED, /* 3 */ HETV, /* 4 */ PJAT /* 5 */ ) rab_ned ;

In this example, an enumerable tag week is declared, with a corresponding set of values, and a variable rab_ned is declared of type week.

The second format uses the enum tag name to refer to an enumeration type defined somewhere else. The enumeration tag name must refer to an already defined enumeration tag within the current scope. Because the enumeration tag is declared somewhere else, the enumeration list is not represented in the declaration.

In declaring a pointer to an enumeration data type and declaring typedefs for enumeration types, you can use the name of an enumeration tag before that enumeration tag is defined. However, the enum definition must precede any use of the typedef declaration's pointer to the type. A declaration without a subsequent list of descriptors describes a tag, or, so to speak, an enumeration pattern.

1.2.6. Arrays

Arrays are a group of elements of the same type (double, float, int, etc.). From the array declaration, the compiler must obtain information about the type of array elements and their number. An array declaration has two formats:

type-specifier descriptor [const - expression];

type-specifier descriptor ;

The descriptor is the identifier of the array.

The type-specifier specifies the type of the elements of the declared array. Array elements cannot be functions or void elements.

The constant expression in square brackets specifies the number of elements in the array. When declaring an array, a constant expression can be omitted in the following cases:

When declared, the array is initialized,

The array is declared as a formal parameter of the function,

SI defines only one-dimensional arrays, but since an element of an array can be an array, multidimensional arrays can also be defined. They are formalized by a list of constant-expressions following the array identifier, with each constant-expression enclosed in its own square brackets.

Each constant-expression in square brackets specifies the number of elements along that dimension of the array, so that a two-dimensional array declaration contains two constant-expressions, a three-dimensional array contains three, and so on. Note that in SI, the first element of an array has an index of 0.

Int a; /* represented as a matrix a a a a a a */ double b; /* vector of 10 elements of type double */ int w = ( ( 2, 3, 4 ), ( 3, 4, 8 ), ( 1, 0, 9 ) );

In the last example, the array w is declared. Lists enclosed in curly braces correspond to array strings; if there are no braces, initialization will not be performed correctly.

In the SI language, you can use array sections, as in other high-level languages ​​(PL1, etc.), however, a number of restrictions are imposed on the use of sections. Sections are formed by omitting one or more pairs of square brackets. Pairs of square brackets can only be dropped from right to left and strictly sequentially. Sections of arrays are used to organize the computational process in SI functions developed by the user.

If you write s when calling a function, then the zero string of the array s will be passed.

When accessing an array b, you can write, for example, b and a vector of four elements will be transferred, and accessing b will give a two-dimensional array of size 3 by 4. You cannot write b, implying that a vector will be transferred, because this does not comply with the restriction imposed on the use array sections.

An example of a character array declaration.

char str = "character array declaration";

Note that there is one more element in a character literal, since the last element is the escape sequence "\0".

1.2.7. Structures

Structures are a composite object that includes elements of any type, with the exception of functions. Unlike an array, which is a homogeneous object, a structure can be heterogeneous. The structure type is determined by an entry of the form:

struct (definition list)

At least one component must be specified in the structure. The definition of structures is as follows:

data-type descriptor;

where data-type specifies the type of structure for the objects defined in the descriptors. In their simplest form, handles are identifiers or arrays.

Struct ( double x,y; ) s1, s2, sm;

struct (int year; char moth, day; ) date1, date2;

Variables s1, s2 are defined as structures, each of which consists of two components x and y. The variable sm is defined as an array of nine structures. Each of the two variables date1, date2 consists of three components year, moth, day. >p>There is another way to associate a name with a structure type, it is based on the use of a structure tag. A structure tag is similar to an enum type tag. The structure tag is defined as follows:

struct tag(list of descriptions; );

where tag is an identifier.

The example below describes the student identifier as a structure tag:

The structure tag is used to subsequently declare structures of this type in the form:

struct id-list tag;

struct studeut st1,st2;

The use of structure tags is necessary to describe recursive structures. The following discusses the use of recursive structure tags.

Struct node ( int data; struct node * next; ) st1_node;

The structure tag node is indeed recursive since it is used in its own description, i.e. in the formalization of the next pointer. Structures cannot be directly recursive, i.e. a node structure cannot contain a component that is a node structure, but any structure can have a component that is a pointer to its type, as is done in the example above.

Structure components are accessed by specifying the structure name and the following, separated by a dot, name of the selected component, for example:

St1.name="Ivanov";

st2.id=st1.id;

st1_node.data=st1.age;

1.2.8. Combinations (mixtures)

A union is similar to a structure, but only one of the elements of the union can be used (or in other words responded to) at any time. The join type can be specified as follows:

Union ( element 1 description; ... element n description; );

The main feature of a union is that the same memory area is allocated for each of the declared elements, i.e. they overlap. Although this memory region can be accessed using any of the elements, the element for this purpose must be selected so that the result obtained is not meaningless.

Members of a union are accessed in the same way as structures. The union tag can be formalized in exactly the same way as the structure tag.

The association is used for the following purposes:

Initializing a memory object in use if at any given time only one of many objects is active;

Interpret the underlying representation of an object of one type as if that object were assigned a different type.

When using an infor object of type union, you can process only the element that received the value, i.e. After assigning a value to the inform.fio element, there is no point in accessing other elements. The ua concatenation allows separate access to the low ua.al and high ua.al bytes of the two-byte number ua.ax .

1.2.9. Bit fields

An element of the structure can be a bit field that provides access to individual bits of memory. Bit fields cannot be declared outside structures. You also cannot organize arrays of bit fields and you cannot apply the address determination operation to the fields. In general, the type of structure with a bit field is specified as follows:

Struct ( unsigned identifier 1: field-length 1; unsigned identifier 2: field-length 2; )

length - fields are specified by an integer expression or constant. This constant specifies the number of bits allocated to the corresponding field. A zero-length field indicates alignment to the next word boundary.

Struct ( unsigned a1: 1; unsigned a2: 2; unsigned a3: 5; unsigned a4: 2; ) prim;

Bit field structures can also contain signed components. Such components are automatically placed on appropriate word boundaries, and some word bits may remain unused.

1.2.10. Variables with mutable structure

Very often, some program objects belong to the same class, differing only in some details. Consider, for example, the representation of geometric shapes. General information about shapes may include elements such as area, perimeter. However, the corresponding geometric dimension information may differ depending on their shape.

Consider an example in which information about geometric shapes is represented based on the combined use of structure and union.

Struct figure ( double area,perimetr; /* common components */ int type; /* component attribute */ union /* enumeration of components */ ( double radius; /* circle */ double a; /* rectangle */ double b; /* triangle */ ) geom_fig; ) fig1, fig2;

In general, each figure object will consist of three components: area, perimetr, type. The type component is called the active component label because it is used to indicate which component of the geom_fig union is currently active. This structure is called a variable structure because its components change depending on the value of the active component's label (the value of type).

Note that instead of an int type component, it would be advisable to use an enumerated type. For example, like this

Enum figure_chess ( CIRCLE, BOX, TRIANGLE ) ;

The constants CIRCLE, BOX, TRIANGLE will receive values ​​equal to 0, 1, 2, respectively. The type variable can be declared as having an enumerated type:

enum figure_chess type;

In this case, the SI compiler will warn the programmer about potentially erroneous assignments, such as

figure.type = 40;

In general, a structure variable will consist of three parts: a set of common components, a label for the active component, and a part with changing components. The general form of a variable structure is as follows:

Struct (common components; active component label; union (component description 1; component description 2; ::: component description n; ) union-identifier; ) structure-identifier;

Example of defining a structure variable named helth_record

Struct ( /* general information */ char name ; /* name */ int age; /* age */ char sex; /* gender */ /* active component label */ /* (marital status) */ enum merital_status ins ; /* variable part */ union ( /* single */ /* no component */ struct ( /* married */ char spouse_name; int no_children; ) marriage_info; /* divorced */ char date_divorced; ) marital_info; ) health_record;

enum marital_status ( SINGLE, /* single */ MARRIGO, /* married */ DIVOREED /* divorced */ ) ;

You can access the components of the structure using links:

Helth_record.neme, helth_record.ins, helth_record.marriage_info.marriage_date .

1.2.11. Defining Objects and Types

As mentioned above, all variables used in SI programs must be declared. The type of variable being declared depends on which keyword is used as the type specifier and whether the specifier is a simple identifier or a combination of an identifier with a pointer (asterisk), array (square brackets), or function (parentheses) modifier.

Let us note an important feature of the SI language: when declaring, more than one modifier can be used simultaneously, which makes it possible to create many different complex type descriptors.

However, we must remember that some combinations of modifiers are unacceptable:

Array elements cannot be functions.

Functions cannot return arrays or functions.

When initializing complex descriptors, square brackets and parentheses (to the right of the identifier) ​​take precedence over the asterisk (to the left of the identifier). Square or parentheses have the same precedence and are expanded from left to right. The type specifier is considered at the last step, when the descriptor has already been fully interpreted. You can use parentheses to change the order of interpretation as needed.

For interpreting complex descriptions, a simple rule is proposed that sounds like "from the inside out", and consists of four steps.

1. Start with the identifier and look to the right to see if there are square or parentheses.

2. If they are, then interpret this part of the descriptor and then look to the left in search of an asterisk.

3. If at any stage a closing parenthesis is encountered on the right, then all these rules must first be applied inside the parentheses and then the interpretation continues.

4. Interpret the type specifier.

Int * (* comp ) ();

6 5 3 1 2 4

This example declares the variable comp (1) as an array of ten (2) pointers (3) to functions (4) returning pointers (5) to integer values ​​(6).

Char * (* (*var) ()) ;

7 6 4 2 1 3 5

The variable var (1) is declared as a pointer (2) to a function (3) that returns a pointer (4) to an array (5) of 10 elements, which are pointers (6) to char values.

Note that any type can be declared using the typedef keyword, including pointer, function, or array types. A name with the typedef keyword for pointer, structure, and union types can be declared before those types are defined, but within the scope of the declarer.

Typedef double(*MATH)();

/* MATH - new type name representing a pointer to a function that returns double values ​​*/ MATH cos;

/* cos pointer to a function returning values ​​of type double */ /* An equivalent declaration can be made */ double (* cos)();

typedef char FIO /* FIO - array of forty characters */ FIO person;

/* The person variable is an array of forty characters */ /* This is equivalent to a declaration */ char person;

When declaring variables and types, type names (MATH FIO) were used here. In addition, type names can be used in three other cases: in the list of formal parameters, in function declarations, in type casting operations, and in the sizeof operation (type casting operation).

The type names for basic, enumeration, structure, and mixture types are the type specifiers for those types. The type names for array pointer and function types are specified using abstract descriptors as follows:

abstract-descriptor-type-specifier;

An abstract-handler is a non-identifier handle consisting of one or more pointer, array, or function modifiers. The pointer modifier (*) is always given before the identifier in the descriptor, and the array and function modifiers () are given after it. Thus, to correctly interpret an abstract descriptor, one must begin the interpretation with the implied identifier.

Abstract descriptors can be complex. The parentheses in complex abstract descriptors specify the order of interpretation in a similar way to the interpretation of complex descriptors in declarations.

1.2.12. Data Initialization

When a variable is declared, it can be assigned an initial value by appending an initiator to a declarator. The initiator begins with the "=" sign and has the following forms.

Format 1: = initiator;

Format 2: = (list - initiators);

Format 1 is used when initializing variables of basic types and pointers, and format 2 is used when initializing composite objects.

A two-dimensional array of b integer values ​​is initialized; the array elements are assigned values ​​from the list. The same initialization can be done like this:

static int b = ( ( 1,2 ), ( 3,4 ) );

When initializing an array, you can omit one or more dimensions

static int b)