Data types and operations in the C language. expressions. Variables in the C language. Declaring a variable in C

In this lesson you will learn C++ language alphabet, and also what data types the program can process it. This may not be the most exciting moment, but this knowledge is necessary! In addition, when you start learning any other programming language, you will go through a similar stage of learning with greater confidence. A C++ program can contain the following symbols:

  • uppercase, lowercase Latin letters A, B, C..., x, y, z and underscore;
  • Arabic numerals from 0 to 9;
  • special characters: ( ) , | , () + - / % * . \‘ : ?< > = ! & # ~ ; ^
  • space, tab, and newline characters.

In program testing you can use comments. If the text contains two forward slash characters // and ends with a newline character or is enclosed between the characters /* and */, then the compiler ignores it.

Data in C++

To solve a problem, any program processes some data. They can be of various types: integers and real numbers, characters, strings, arrays. In C++, data is usually described at the beginning of the function. TO basic data types languages ​​include:

To generate other types of data, basic and so-called specifiers. C++ defines four data type specifiers:

  • short - short;
  • long - long;
  • signed - signed;
  • unsigned - unsigned.

Integer type

Type variable int in computer memory can occupy either 2 or 4 bytes. It depends on the processor bit size. By default, all integer types are considered signed, that is, the specifier signed may not be specified. Specifier unsigned allows you to represent only positive numbers. Below are some ranges of integer type values

Type Range Size
int -2147483648…2147483647 4 bytes
unsigned int 0…4294967295 4 bytes
signed int -2147483648…2147483647 4 bytes
short int -32768…32767 2 bytes
long int -2147483648…2147483647 4 bytes
unsigned short int 0…65535 2 bytes

Real type

A floating point number is represented in the form mE +- p, where m is the mantissa (integer or fractional number with a decimal point), p is the exponent (integer). Typically values ​​like float takes up 4 bytes, and double 8 bytes. Real value range table:

float 3.4E-38…3.4E+38 4 bytes
double 1.7E-308…1.7E+308 8 bytes
long double 3.4E-4932…3.4E+4932 8 bytes

Boolean type

Type variable bool can only take two values true ( true ) or fasle ( lie ). Any value not equal to zero is interpreted as true. Meaning false represented in memory as 0.

type void

The value set of this type is empty. It is used to define functions that do not return a value, to specify an empty list of function arguments, as the base type for pointers, and in type casting operations.

Data type conversion

In C++, there are two types of data type conversion: explicit and implicit.

  • Implicit conversion happens automatically. This is done during comparison, assignment, or evaluation of expressions of various types. For example, the following program will print to the console a value like float

#include "stdafx.h" #include using namespace std; int main() ( int i=5; float f=10.12; cout<>void"); return 0; )

#include "stdafx.h"

#include

using namespace std ;

int main()

int i = 5 ; float f = 10.12 ;

cout<< i / f ;

system ("pause>>void" ) ;

return 0 ;

The highest priority is given to the type in which the least amount of information is lost. You should not abuse implicit type conversion, as various unforeseen situations may arise.

  • Explicit conversion in contrast to implicit, it is carried out by the programmer. There are several ways to do this conversion:
  1. Converting to Styles C: (float) a
  2. Converting to Styles C++: float()

Type casting can also be performed using the following operations:

static_cast<>() const_cast<>() reinterpret_cast<>() dynamic_cast<> ()

static_cast<> ()

const_cast<> ()

reinterpret_cast<> ()

dynamic_cast<> ()

static_cas- converts related data types. This operator casts types according to the usual rules, which may be necessary when the compiler does not perform automatic conversion. The syntax will look like this:

Type static_cast<Тип>(an object);

Using static_cast, you cannot remove constancy from a variable, but the following operator can do it. const_cast- is used only when it is necessary to remove constancy from an object. The syntax will look like this:

Typeconst_cast< Type> (an object);

reinterpret_cast- used to convert different types, integer to pointer and vice versa. If you see a new word "index" - don't be alarmed! This is also a data type, but we won’t be working with it soon. The syntax here is the same as that of the previously discussed operators:

Typereinterpret_cast< Type> (an object);

dynamic_cast- used for dynamic type conversion, implements pointer or reference casting. Syntax:

Typedynamic _cast< Type> (an object);

Control characters

You are already familiar with some of these “control characters” (for example, with \n). They all start with a backslash and are also surrounded by double quotes.

Image

Hex code

Name

Beeper sound

Go back one step

Translation of page (format)

Line translation

Carriage return

Horizontal tabulation

Vertical tab

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, such as 3.14159 or pi, and then the concept is introduced variable, or 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 basic C++ types.

3.1. Literals

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 intended 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.

When a certain number, for example 1, is encountered in a program, this number is called literal, or 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
0x14 // hexadecimal

If a literal begins with 0, it is treated as octal, if with 0x or 0X, then as hexadecimal. The usual notation is treated as a decimal number.
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:

"" (empty string) "a" "\nCC\toptions\tfile.\n" "a multi-line \ string literal signals its \ continuation with a backslash"

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
cout<< "2 raised to the power of 10: ";
cout<< 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2;
cout<< endl;
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:

Cout<< "2 в степени X\t"; cout << 2 * ... * 2;

where X is successively increased by 1, and the required number of literals is substituted for the decimal?

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 stage. 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:

#include
int main()
{
// objects of type int
int value = 2;
int pow = 10;
cout<< 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;
cout<< 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:

#include extern int pow(int,int); int main() ( int val = 2; int exp = 15;
cout<< "Степени 2\n";
for (int cnt=0; cnt<= exp; ++cnt)
cout<< 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

Variable, or 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) - the place where the r-value is stored; inherent only in the object.

In expression

Ch = ch - "0";

the variable ch is located both to the left and to the right of the assignment symbol. On the right is the value to read (ch and the character literal "0"): the data associated with the variable is read from the corresponding memory area. On the left is the location value: the result of the subtraction is placed in the memory area associated with the variable ch. In general, the left operand of an assignment operation must be an l-value. We cannot write the following expressions:

// compilation errors: values ​​on the left are not l-values ​​// error: literal is not an l-value 0 = 1; // error: arithmetic expression is not an l-value salary + salary * 0.10 = new_salary;

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 case
catch char class const const_cast
continue default delete do double
dynamic_cast else enum explicit export
extern false float for friend
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();

In the following definition:

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

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 #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

Explain the difference between l-value and r-value. Give examples.

Exercise 3.5

Find the differences in the use of the name and student variables in the first and second lines of each example:

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

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< int >_; (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:

Float fp, *fp2;

The dereference operator (*) can be separated by spaces from the name and even directly adjacent to the type keyword. Therefore, the above definitions are syntactically correct and completely equivalent:

//attention: ps2 is not a pointer to a string! string* ps, ps2;

It can be assumed that both ps and ps2 are pointers, although the pointer is only the first of them.
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 pointer of one type to a value 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*
int *pi =
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?

Exercise 3.10

This program contains an error related to incorrect use of pointers:

Int foobar(int *pi) ( *pi = 1024; return *pi; )
int main() (
int *pi2 = 0;
int ival = foobar(pi2);
return 0;
}

What is the error? How can I fix it?

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:

#include

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 becomes 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 operator ++ 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. The task is simple: calculate the length of the string. The first version is incorrect. Fix her.

#include const char *st = "Price of a bottle of wine\n"; int main() (
int len ​​= 0;
while (st++) ++len; cout<< len << ": " << st;
return 0;
}

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.
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?

#include const char *st = "Price of a bottle of wine\n"; int main()
{
int len ​​= 0;
while (*st++) ++len; cout<< len << ": " << st << endl;
return 0;
}

The error is that after the loop ends, the st pointer 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 output will be a random sequence of characters.
You can try to fix this error:

St = st – len; cout<< len << ": " << st;

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;

and here is the correct result:

18: Price of a bottle of wine

However, we cannot say that our program looks elegant. Operator

St = st – len - 1;

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 the 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 type) 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;
  • ability to find out if a string is empty. For built-in strings, two conditions have to be checked for this purpose: char str = 0; //... if (! str || ! *str) return;

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:

#include

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

#include 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).

Cout<< "Длина " << 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 #include
int main()
{
int errors = 0;
const char *pc = "a very long literal string"; for (int ix = 0; ix< 1000000; ++ix)
{
int len ​​= strlen(pc);
char *pc2 = new char[ len + 1 ];
strcpy(pc2, pc);
if (strcmp(pc2, pc))
++errors; delete pc2;
}
cout<< "C-строки: "
<< errors << " ошибок.\n";
) // ***** Implementation using the string class ***** #include
#include
int main()
{
int errors = 0;
string str("a very long literal string"); for (int ix = 0; ix< 1000000; ++ix)
{
int len ​​= str.size();
string str2 = str;
if (str != str2)
}
cout<< "класс string: "
<< errors << " ошибок.\n;
}

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:

For (int index = 0; index< 512; ++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.

Index< bufSize

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.

Int bufSize = 512; // input buffer size // ... for (int index = 0; index< bufSize; ++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

Const int bufSize = 512; // input buffer size

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 =

A constant pointer does not allow the object it addresses to be modified using indirect addressing. Although dval in the example above is not a constant, the compiler will not allow dval to be changed via pc. (Again, because it is not able to determine which object's address may contain a pointer at any time during program execution.)
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, sometimes called an alias, is used to give an object an additional name. A reference allows you to manipulate an object indirectly, just like you can with a pointer. However, this indirect manipulation does not require the special syntax required for pointers. Typically, references are used as formal parameters of functions. In this section, we will look at using reference type objects on our own.
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; // only true 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

Are there any errors in these definitions? Explain. How would you fix them?

(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;
// ... // shorthand: if (found == true)
if (found)
cout<< "ok, мы нашли слово\n";
else cout<< "нет, наше слово не встретилось.\n";

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:

Bool open_file(string file_name, int open_mode); // ...
open_file("Phoenix_and_the_Crane", append);

This solution is possible, but not entirely acceptable, since we cannot guarantee that the argument passed to the open_file() function is only 1, 2 or 3.
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; // ... om = append; open_file("TailTell", om);

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

Cout<< input << " " << om << endl;

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:

Cout<< open_modes_table[ input ] << " " << open_modes_table[ om ] << endl Будет выведено: input append

In addition, you cannot iterate over all the values ​​of an enum:

// not supported for (open_modes iter = input; iter != append; ++inter) // ...

To define an enumeration, use the enum keyword, and element names are specified in curly braces, separated by commas. By default, the first one is 0, the next one is 1, and so on. You can change this rule using the assignment operator. In this case, each subsequent element without an explicitly specified value will be 1 more than the element that comes before it in the list. In our example, we explicitly specified the value 1 for input, and output and append will be equal to 2 and 3. Here is another example:

// shape == 0, sphere == 1, cylinder == 2, polygon == 3 enum Forms( share, spere, cylinder, polygon );

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 the 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
const int buf_size = 512, max_files = 20;
int staff_size = 27; // correct: constant char input_buffer[ buf_size ]; // correct: constant expression: 20 - 3 char *fileTable[ max_files-3 ]; // error: not a constant double salaries[ staff_size ]; // error: not a constant expression int test_scores[ get_size() ];

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 array elements 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()
{
int ia3( array_size ]; // correct
// error: built-in arrays cannot be copied
ia3 = ia;
return 0;
}

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 ]; for (int ix = 0; ix< array_size; ++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

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<= array_size; ++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 ) );

Inner curly braces, which break the list of values ​​into lines, are optional and are generally used to make the code easier to read. The initialization below is exactly the same as the previous example, although it is less clear:

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

The following definition initializes only the first elements of each line. The remaining elements will be zero:

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

If you omit the inner curly braces, the result will be completely different. All three elements of the first row and the first element of the second will receive the specified value, and the rest will be implicitly initialized to 0.

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). 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.

You can traverse an array using an index, as we did in the previous section, or using pointers. For example:

#include int main() ( int ia = ( 0, 1, 1, 2, 3, 5, 8, 13, 21 ); int *pbegin = ia; int *pend = ia + 9; while (pbegin != pend) ( cout<< *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 becomes equal to 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:

#include void ia_print(int *pbegin, int *pend) (
while (pbegin != pend) (
cout<< *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):

#include template void print(elemType *pbegin, elemType *pend) ( while (pbegin != pend) ( cout<< *pbegin << " "; ++pbegin; } }

We can now 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:

#include 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:

#include

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 >ivec(10);

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); int ia[ e1em_size ]; for (int ix = 0; ix< e1em_size; ++ix)
ia[ ix ] = 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:

Void print_vector(vector ivec) ( if (ivec.empty()) return; for (int ix=0; ix< ivec.size(); ++ix)
cout<< ivec[ 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:

Vector< int >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 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. As a list of initial values, it is permissible to specify not the entire array, but a certain range of it:

// 3 elements are copied: ia, ia, ia vector< int >ivec(&ia[ 2 ], &ia[ 5 ]);

Another difference between a vector and a built-in array is the ability to initialize one vector object with another and use the assignment operator to copy objects. For example:

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;
}

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:

Vector< string >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; while (cin >> word) ( text.push_back(word); // ... )

Although we can use the index operation to iterate over the elements of a vector:

Cout<< "считаны слова: \n"; for (int ix =0; ix < text.size(); ++ix) cout << text[ ix ] << " "; cout << endl;

A more typical use of iterators within this idiom would be:

Cout<< "считаны слова: \n"; for (vector::iterator it = text.begin(); it != text.end(); ++it) cout<< *it << " "; cout << endl;

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

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:

Vector ivec;

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:

Vector ia(10);

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< int >ivec(size); for (int ix = 0; ix< size; ++ix) ivec.push_back(ia[ ix ]);

This meant initializing the ivec vector with the values ​​of the ia elements, which resulted in an ivec vector of size 14.
Following the STL idiom, you can not only add but also remove elements from a vector. (We will look at all this in detail and with examples in Chapter 6.)

Exercise 3.24

Are there any errors in the following definitions?
int ia[ 7 ] = ( 0, 1, 1, 2, 3, 5, 8 );

(a)vector< vector< int >>ivec;
(b)vector< int >ivec = ( 0, 1, 1, 2, 3, 5, 8 );
(c)vector< int >ivec(ia, ia+7);
(d)vector< string >svec = ivec;
(e)vector< string >svec(10, string("null"));

Exercise 3.25

Implement the following function:
bool is_equal(const int*ia, int ia_size,
const vector &ivec);
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. As usual, to use it you need to include the header file:

#include

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:

// purely imaginary number: 0 + 7-i complex< double >purei(0, 7); // imaginary part is 0: 3 + Oi complex< float >rea1_num(3); // both real and imaginary parts are equal to 0: 0 + 0-i complex< long double >zero; // initialization of one complex number with another complex< double >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:

Complex< double >conjugate[ 2 ] = ( complex< double >(2, 3), complex< double >(2, -3) };

Complex< double >*ptr = complex< double >&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 test0(c1ass_size); const int c1ass_size = 34; test_scores test0(c1ass_size); //vector< bool >attendance; vector< in_attendance >attendance(c1ass_size); // int *table[ 10 ]; Pint table [10];

This directive begins with the typedef keyword, followed by a type specifier, and ends with an identifier, which becomes a synonym for the specified type.
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 is a 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.

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. These values ​​can be the same or different types. To use this class you must include the header file:

#include

For example, instructions

Pair< string, string >author("James", "Joyce");

creates an author object of type pair, consisting of two string values.
The individual parts of a pair can be obtained using the first and second members:

String firstBook; if (Joyce.first == "James" &&
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:

#include 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

String str2("character string");

The constructor is called

String(const char*);

String str3(str2);

Constructor

String(const String&);

The type of the constructor called is determined by the type of the actual argument. The last of the constructors, String(const String&), is called a copy constructor because it initializes an object with a copy of another object.
If you write:

String str4(1024);

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:

// contents of the source file: String.C // enabling the definition of the String class
#include "String.h" // include the definition of the strcmp() function
#include
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;
return strcmp(_strinq, rhs._string) ?
false: true;
}

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.

#include // 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:

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

The operation of taking an index is almost identical to its implementation for the Array array that we created in section 2.3:

#include inline char&
String::operator (int elem)
{
assert(elem >= 0 && elem< _size);
return _string[ elem ];
}

Input and output operators are implemented as separate functions rather than class members. (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 appropriate header file:

#include 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*);

The output operator needs access to the internal representation of the String. Since operator<< не является функцией-членом, он не имеет доступа к закрытому члену данных _string. Ситуацию можно разрешить двумя способами: объявить operator<< дружественным классу String, используя ключевое слово friend (дружественные отношения рассматриваются в разделе 15.2), или реализовать встраиваемую (inline) функцию для доступа к этому члену. В нашем случае уже есть такая функция: c_str() обеспечивает доступ к внутреннему представлению строки. Воспользуемся ею при реализации операции вывода:

Inline ostream& operator<<(ostream& os, const String &s) { return os << s.c_str(); }

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 #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"); // operator>>(ostream&, String&)
while (cin >> buf) (
++wdCnt; // operator<<(ostream&, const String&)
cout<< buf << " "; if (wdCnt % 12 == 0)
cout<< endl; // String::operator==(const String&) and
// String::operator==(const char*);
if (buf == the | | buf == "The")
++theCnt;
else
if (buf == it || buf == "It")
++itCnt; // invokes String::s-ize()
for (int ix =0; ix< buf.sizeO; ++ix)
{
// invokes String::operator(int)
switch(buf[ ix ])
{
case "a": case "A": ++aCnt; break;
case "e": case "E": ++eCnt; break;
case "i": case "I": ++iCnt; break;
case "o": case "0": ++oCnt; break;
case "u": case "U": ++uCnt; break;
default: ++notVowe1; break;
}
}
) // operator<<(ostream&, const String&)
cout<< "\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;
}

Let's test the program: we will offer it a paragraph from a children's story written by one of the authors of this book (we will meet with this story in Chapter 6). Here is the result of the program:

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; // ... );

Variables are used to store various data in programming languages. A variable is an area of ​​memory that has a name, otherwise called an identifier.

By giving a variable a name, the programmer at the same time names the memory area where the variable’s values ​​will be written for storage.

It is good style to name variables meaningfully. It is allowed to use lowercase and uppercase letters, numbers and the underscore, which is considered a letter in C. The first character must be a letter, and there must be no spaces in the variable name. In modern versions of compilers, the name length is practically unlimited. The variable name cannot match reserved keywords. Uppercase and lowercase letters in variable names are different, variables a And A- different variables.

Reserved Keywords auto double int struct break else long switch register tupedef char extern return void case float unsigned default for signed union do if sizeof volatile continue enum short while
In C, all variables must be declared. This means that, firstly, at the beginning of each program or function you must provide a list of all the variables used, and secondly, indicate the type of each of them.

When a variable is declared, the compiler allocates memory space for it depending on its type. Using standard AVR GCC tools, it works with data types char(character type) and int(integer type).

Variable types

Type char

char- is the most economical type. The char type can be signed or unsigned. Denoted accordingly as " signed char" (signed type) and " unsigned char" (unsigned type). The signed type can store values ​​in the range from -128 to +127. Unsigned - from 0 to 255. A char variable has 1 byte of memory (8 bits).

Keywords (modifiers) signed And unsigned 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.

Type int

Integer value int May be short(short) or long(long).

Keyword (modifier) short placed after keywords signed or unsigned. Thus, the following types are distinguished: signed short int, unsigned short int, signed long int, unsigned long int.

Type variable signed short int(signed short integer) can take values ​​from -32768 to +32767, unsigned short int(unsigned short integer) - from 0 to 65535. Exactly two bytes of memory (16 bits) are allocated for each of them.

When declaring a type variable signed short int keywords signed And short can be omitted, and such a variable type can be declared simply int. It is also possible to declare this type with one keyword short.

Variable unsigned short int can be declared as unsigned int or unsigned short.

For every size signed long int or unsigned long int 4 bytes of memory are allocated (32 bits). The values ​​of variables of this type can be in the ranges from -2147483648 to 2147483647 and from 0 to 4294967295, respectively.

There are also variables like long long int, for which 8 bytes of memory are allocated (64 bits). They can also be signed or unsigned. For a signed type, the range of values ​​is from -9223372036854775808 to 9223372036854775807, for an unsigned type - from 0 to 18446744073709551615. A signed type can be declared simply by two keywords long long.

Type Range Hex range Size
unsigned char 0 ... 255 0x00...0xFF 8 bit
signed char
or simply
char
-128 ... 127 -0x80...0x7F 8 bit
unsigned short int
or simply
unsigned int or unsigned short
0 ... 65535 0x0000 ... 0xFFFF 16 bit
signed short int or signed int
or simply
short or int
-32768 ... 32767 0x8000 ... 0x7FFF 16 bit
unsigned long int
or simply
unsigned long
0 ... 4294967295 0x00000000 ... 0xFFFFFFFF 32 bit
signed long
or simply
long
-2147483648 ... 2147483647 0x80000000 ... 0x7FFFFFFF 32 bit
unsigned long long 0 ... 18446744073709551615 0x0000000000000000 ... 0xFFFFFFFFFFFFFFFF 64 bit
signed long long
or simply
long long
-9223372036854775808 ... 9223372036854775807 0x8000000000000000 ... 0x7FFFFFFFFFFFFFF 64 bit

Variables are declared in a declaration statement. A declaration statement consists of a type specification and a comma-separated list of variable names. There must be a semicolon at the end.

A variable declaration has the following format:

[modifiers] type_specifier identifier [, identifier] ...

Modifiers- keywords signed, unsigned, short, long.
Type specifier- keyword char or int, which determines the type of the declared variable.
Identifier- variable name.

Example: char x; int a, b, c; unsigned long long y;
This way the variables will be declared x, a, b, c, y. To a variable x it will be possible to write values ​​from -128 to 127. In variables a, b, c- from -32768 to +32767. To a variable y- from 0 to 18446744073709551615.

Initializing the value of a variable upon declaration

When declared, a variable can be initialized, that is, assigned an initial value. You can do this as follows. int x = 100; Thus, into the variable x When announced, the number 100 will be immediately written down.

It is better to avoid mixing initialized variables in one declaration statement, that is, it is better to declare initialized variables on separate lines.

Constants

A variable of any type can be declared unmodifiable. This is achieved by adding the keyword const to the type specifier. Variables with type const is read-only data, meaning that the variable cannot be assigned a new value. If after the word const If there is no type specifier, then constants are treated as signed values ​​and are assigned a type int or long int according to the value of the constant: if the constant is less than 32768, then it is assigned the type int, otherwise long int.

Example: const long int k = 25; const m = -50; // implied const int m=-50 const n = 100000; // implied const long int n=100000

Assignment

The "=" sign is used for assignment in C. The expression to the right of the assignment sign is evaluated, and the resulting value is assigned to the variable to the left of the assignment sign. In this case, the previous value stored in the variable is erased and replaced with a new one.

The "=" operator should not be understood as equality.
For example, the expression a = 5; should be read as "assign variable a to 5".

Examples: x = 5 + 3; // add the values ​​5 and 3, // assign the result to variable x (write to variable x) b = a + 4; // add 4 to the value stored in variable a, // assign the resulting result to variable b (write to variable b) b = b + 2; // add 2 to the value stored in variable b, // assign the resulting result to variable b (write to variable b)
On the right side, the value of the variable can be used several times: c = b * b + 3 * b;

Example: x = 3; // variable x will be assigned the value 3 y = x + 5; // the number 5 will be added to the value stored in the x variable, // the resulting result will be written to the y variable z = x * y; // the values ​​of the x and y variables will be multiplied, // the result will be written to the z variable z = z - 1; // 1 will be subtracted from the value stored in the z variable // the result will be written to the z variable
Thus, in the variable z the number 23 will be stored

In addition to the simple assignment operator "=", there are several more combined assignment operators in C: "+=", "-=", "*=
Examples: x += y; // same as x = x + y; - add x and y // and write the result to the variable x x -= y; // same as x = x - y; - subtract the value y from x // and write the result to the variable x x *= y; // same as x = x * y; - multiply x by y // and write the result to the variable x x /= y; // same as x = x / y; - divide x by y // and write the result to the variable x x %= y; // same as x = x % y; // calculate the integer remainder when dividing x by y // and write the result to the variable x

Increment and decrement

If you need to change the value of a variable to 1, then use increment or decrement.

Increment- the operation of increasing the value stored in a variable by 1.

Example: x++; // the value of the variable x will be increased by 1$WinAVR = ($_GET["avr"]); if($WinAVR) include($WinAVR);?>
Decrement- the operation of decreasing the value stored in a variable by 1.

Example: x--; // the value of the variable x will be decreased by 1
Increment and decrement are assignment operators. When using decrement and increment together with the assignment operator "=", use postfix (x++) or prefix (++x) notation. The prefix entry is executed first.

Examples: y = x++;
Let us assume that in the variable x the value 5 was stored. Then in y the value 5 will be written, after which the value of the variable x will be increased by 1. Thus, in y will be 5, and in x- 6. y = --x;
If in x If the value 5 was stored, then the decrement will be performed first x to 4 and then this value will be assigned to the variable y. Thus, x And y will be assigned the value 4.

Data types

Data types are especially important in C# because it is a strongly typed language. This means that all operations are subject to strict type checking by the compiler, and illegal operations are not compiled. Therefore, strict type checking eliminates errors and increases the reliability of programs. To enforce type checking, all variables, expressions, and values ​​must be of a specific type. There is no such thing as a “typeless” variable in this programming language at all. Moreover, the type of a value determines the operations that can be performed on it. An operation that is legal for one data type may not be valid for another.

There are two general categories of built-in data types in C#: value types And reference types. They differ in the contents of the variable. Conceptually, the difference between the two is that a value type stores data directly, while a reference type stores a reference to a value.

These types are stored in different locations in memory: value types are stored in an area known as the stack, and reference types are stored in an area known as the managed heap.

Let's take a look value types.

Integer types

C# defines nine integer types: char, byte, sbyte, short, ushort, int, uint, long and ulong. But the char type is used primarily to represent characters and is therefore treated separately. The remaining eight integer types are for numeric calculations. Below are their range of numbers and bit depth:

C# Integer Types
Type Type CTS Bit size Range
byte System.Byte 8 0:255
sbyte System.SByte 8 -128:127
short System.Int16 16 -32768: 32767
ushort System.UInt16 16 0: 65535
int System.Int32 32 -2147483648: 2147483647
uint System.UInt32 32 0: 4294967295
long System.Int64 64 -9223372036854775808: 9223372036854775807
ulong System.UInt64 64 0: 18446744073709551615

As the table above shows, C# defines both signed and unsigned variants of the various integer types. Signed integer types differ from their unsigned counterparts in the way they interpret the most significant bit of the integer. Thus, if a program specifies a signed integer value, the C# compiler will generate code that uses the most significant bit of the integer as the sign flag. A number is considered positive if the sign flag is 0, and negative if it is 1.

Negative numbers are almost always represented by the two's complement method, whereby all the binary digits of the negative number are first inverted and then 1 is added to that number.

Probably the most common integer type in programming is int type. Variables of type int are often used for loop control, array indexing, and general purpose mathematical calculations. When you need an integer value with a larger range of representations than the int type, there are a number of other integer types available for this purpose.

So, if the value needs to be stored without a sign, then for it you can select uint type, for large signed values ​​- long type, and for large unsigned values ​​- type ulong. As an example, below is a program that calculates the distance from the Earth to the Sun in centimeters. To store such a large value, it uses a long variable:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 ( class Program ( static void Main(string args) ( long result; const long km = 149800000; // distance in km. result = km * 1000 * 100; Console.WriteLine(result); Console.ReadLine(); ) ) )

All integer variables can be assigned values ​​in decimal or hexadecimal notations. In the latter case, a 0x prefix is ​​required:

Long x = 0x12ab;

If there is any uncertainty as to whether an integer value is of type int, uint, long, or ulong, then default int is accepted. To explicitly specify what other integer type a value should have, the following characters can be appended to a number:

Uint ui = 1234U; long l = 1234L; ulong ul = 1234UL;

U and L can also be written in lowercase, although a lowercase L can easily be visually confused with the number 1 (one).

Floating Point Types

Floating-point types allow you to represent numbers with a fractional part. There are two types of floating point data types in C#: float And double. They represent numeric values ​​in single and double precision, respectively. Thus, the width of the float type is 32 bits, which approximately corresponds to the range of representation of numbers from 5E-45 to 3.4E+38. And the width of the double type is 64 bits, which approximately corresponds to the range of representation of numbers from 5E-324 to 1.7E+308.

The float data type is intended for smaller floating-point values ​​that require less precision. The double data type is larger than float and offers a higher degree of precision (15 bits).

If a non-integer value is hard-coded in the source code (for example, 12.3), then the compiler usually assumes that a double value is intended. If the value needs to be specified as a float, you will need to append the F (or f) character to it:

Float f = 12.3F;

Decimal data type

A decimal type is also provided to represent high-precision floating-point numbers. decimal, which is intended for use in financial calculations. This type has a width of 128 bits to represent numeric values ​​ranging from 1E-28 to 7.9E+28. You're probably aware that regular floating-point arithmetic is prone to decimal rounding errors. These errors are eliminated by using the decimal type, which allows numbers to be represented to 28 (and sometimes 29) decimal places. Because this data type can represent decimal values ​​without rounding errors, it is especially useful for financial-related calculations:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 ( class Program ( static void Main(string args) ( // *** Calculation of the cost of an investment with *** // *** fixed rate of return*** decimal money, percent; int i; const byte years = 15 ; money = 1000.0m; percent = 0.045m;

The result of this program will be:

Symbols

In C#, characters are represented not in 8-bit code, as in many other programming languages ​​such as C++, but in 16-bit code, called Unicode. Unicode's character set is so broad that it covers characters from almost every natural language in the world. While many natural languages, including English, French and German, have relatively small alphabets, some other languages, such as Chinese, use fairly large character sets that cannot be represented in 8-bit code. To overcome this limitation, C# defines type char, which represents unsigned 16-bit values ​​ranging from 0 to 65,535. However, the standard 8-bit ASCII character set is a subset of Unicode ranging from 0 to 127. Therefore, ASCII characters are still valid in C# .

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 it can be accessed.

1.2.1 Categories of data types

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; unsigned int b; int c; (signed int c is implied); unsigned d; (implies unsigned int d); signed f; (signed int f is implied).

Note that the char type-modifier is used to represent a character (from an array representation of characters) or to declare string literals. The value of a char object is the 1-byte code corresponding to the character it represents. To represent characters of the Russian alphabet, the data identifier type modifier is unsigned char, since the codes of Russian letters exceed the value of 127.

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 size of memory 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); 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.

For example:

0xA8C (int signed); 01786l (long signed); 0xF7u (int unsigned);

1.2.3. Floating data

For variables representing a floating point number, the following type modifiers are used: float, double, long double (in some implementations of the long double language there is no SI).

A value with the float type modifier takes 4 bytes. Of these, 1 byte is allocated for the sign, 8 bits for the excess exponent and 23 bits for the mantissa. Note that the most significant bit of the mantissa is always 1, so it is not filled, so the range of values ​​for a floating point variable is approximately 3.14E-38 to 3.14E+38.

A double value takes up 8 bits of memory. Its format is similar to the float format. The memory bits are distributed as follows: 1 bit for the sign, 11 bits for the exponent, and 52 bits for the mantissa. Taking into account the omitted high bit of the mantissa, the range of values ​​is from 1.7E-308 to 1.7E+308.

Float f, a, b; double x,y;

1.2.4. Signposts

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 pointer size, 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. */

1.2.5. Enumerated variables

A variable that can take a value from some list of values ​​is called an enumerated variable or enumeration.

An enumeration declaration begins with the enum keyword and has two presentation formats.

Format 1. enum [enum-tag-name] (enum-list) descriptor[,descriptor...];

Format 2. enum enum-tag-name descriptor [,descriptor..];

An enumeration declaration specifies the type of the enumeration variable and defines a list of named constants called an enumeration-list. The value of each list name is some integer.

An enumeration type variable can take the value of one of the list's named constants. Named list constants are of type int. Thus, the memory corresponding to an enumeration variable is the memory required to accommodate an int value.

Variables of type enum can be used in index expressions and as operands in arithmetic and relational operations.

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 name of an enumeration tag 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:

Struct student ( char name; int id, age; char prp; );

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.

The memory that corresponds to a union type variable is determined by the amount required to accommodate the longest element of the union. When a shorter element is used, the union type variable may contain unused memory. All elements of a union are stored in the same memory area, starting at the same address.

Union (char fio; char addresses; int vozrast; int telefon; ) inform; union ( int ax; char al; ) ua;

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.

When declaring a simple variable, structure, mixture or union, or enumeration, the handle is a simple identifier. To declare a pointer, array, or function, the identifier is modified accordingly: an asterisk on the left, square or parentheses on the right.

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 within the parentheses must first be applied 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.

In addition to declaring variables of various types, it is possible to declare types. This can be done in two ways. The first way is to specify a tag name when declaring a structure, union, or enumeration, and then use that name in variable and function declarations as a reference to that tag. The second is to use the typedef keyword to declare a type.

When declared with the typedef keyword, the identifier in place of the object being described is the name of the data type being considered, and this type can then be used to declare variables.

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 an "=" 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.

The tol variable is initialized with the character "N".

const long megabute = (1024 * 1024);

The non-modifiable megabute variable is initialized with a constant expression after which it cannot be changed.

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

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)