Партнерка на США и Канаду по недвижимости, выплаты в крипто

  • 30% recurring commission
  • Выплаты в USDT
  • Вывод каждую неделю
  • Комиссия до 5 лет за каждого referral

if( n>(sizeof a)/sizeof(double) )

{ printf("Слишком много элементов\n"); return; }

for(i=0; i<n; i++)

{

printf("a[%d] = ", i); scanf("%lf", &a[i]);

}

/* Операторы, обрабатывающие массив */

}

Во многих случаях удобно возложить на программу подсчет числа элементов, вводимого массива, при этом ввод завершается при появлении во входном потоке признака конца данных. Таким признаком в следующей программе служит число большее 1.0e300

#include <stdio. h>

void main(void)

{

double a[100], temp; int n, end;

for(end=n=0; n<(sizeof a)/sizeof(double); n++)

{

printf("a[%d] = ", n); scanf("%lf", &temp);

if( temp>=1.0e300 ) { end=1; break; }

a[n] = temp;

}

if( end )

{

/* Операторы, обрабатывающие массив */

}

else

printf("Переполнение массива\n");

}

Вывод массива, содержащего большое количество элементов желательно производить в несколько строк с остановом после заполнения экрана.

Следующий фрагмент программы выводит массив строками по 5 элементов. После вывода 120 элементов программа останавливается для просмотра выдачи. Очередные 120 элементов выводятся после нажатия на любую клавишу.

for (i=0; i<n; i++)

{

printf("%10.3lf ", a[i]);

if( (i+6) % 5 == 0 ) printf("\n");

if( (i+121) % 120 == 0 ) { getch(); clrscr(); }

}

Здесь стандартная функция clrscr() очищает экран.

9.3.  Инициализация массива

Инициализация - присвоение значений вместе с описанием данных. Ранее была рассмотрена инициализация простых переменных, например:

НЕ нашли? Не то? Что вы ищете?

int a = 5;

Для инициализации массива за его именем располагают знак присваивания и список инициализации, который представляет собой заключенные в фигурные скобки и разделенные запятыми инициализирующие значения. Ниже приведен пример инициализации массива:

int a[4] = { 15, 21, 1, 304 };

индексы элементов ->

Констант в списке инициализации должно быть не больше, чем объявленный размер массива. Если их меньше, то элементы для которых нет констант обнуляются. Для инициализируемого массива допускается вообще не указывать размер. В этом случае размер массива определяется по количеству констант, например по описанию

int c[] = { 1, 15, 18, 11, 20 };

транслятор выделит 10 байт для хранения массива из 5 двухбайтовых целых чисел.

Частный случай инициализации массива - инициализация строк. Массив символов может быть проинициализирован стандартным образом:

char s[] = { 'A', 'B', 'C', 'D' };

Строка символов дополнительно должна завершаться нуль-символом.

char s[] = { 'A', 'B', 'C', 'D', '\0' };

В связи с тем, что инициализацию строк приходится организовывать довольно часто, язык Си предусматривает для этого упрощенную форму записи:

char s[] = "ABCD";

этом случае нуль-символ автоматически дописывается в конец строки. Два последних примера инициализации строки совершенно эквивалентны.

9.4.  Программа вычисления длины строки символов

В качестве примера использования массива, рассмотрим программу определяющую длину строки символов, вводимой с клавиатуры.

#include <stdio. h>

void main (void)

{

int len;

char str[81];

printf("Введите строку: "); scanf("%s", str);

for(len=0; str[len]; len++);

printf("Длина строки = %d\n", len);

}

В этой программе используется цикл for с пустым оператором тела цикла. Цикл будет выполняться до тех пор, пока в строке не встретится нуль-символ, то есть пока выражение str[len] будет отлично от нуля. После окончания цикла переменная len станет равной количеству символов строки str, исключая нуль-символ.

Фрагмент вычисления длины строки можно оформить в виде отдельной функции и затем использовать в разных программах. Эта функция может выглядеть следующим образом:

int StrLen (char str[])

{

int len;

for(len=0; str[len]; len++);

return len;

}

При наличии функции StrLen два последних оператора предыдущей программы можно заменить одним

printf("Длина строки = %d\n", StrLen(str));

Пример обработки одномерного массива.

Дан массив из 50 целых чисел. Найти наибольший элемент в массиве и его порядковый номер.

# include <stdio. h>

#include <conio. h>

# define n 50 // определение константы n=50

int i, max, nom, a[n]; //описание массива целых чисел из n элементов

main()

{for (i=0; i<n; i++)

{printf( “\n Введите элемент массива ”);

scanf ("%d", &a[i])};

for (i=1,max=a[0],nom=0; i<n; i++)

if (max<a[i])

{nom=i; max=a[i];}

printf("\n Вывод элементов исходного массива : \n");

for (i=0; i<n; i++) printf ( "%6d", a[i] );

printf ("\n Максимальное число в массиве %4d, его индекс %4d " , max, nom);

getch();

}

9.5.  Двумерные массивы (массивы массивов)

Элементом массива может быть в свою очередь тоже массив. Таким образом, мы приходим к понятию двумерного массива или матрицы. Описание двумерного массива строится из описания одномерного путем добавления второй размерности, например:

int a[4][3];

Анализ подобного описания необходимо проводить в направлении выполнения операций [], то есть слева направо. Таким образом, переменная a является массивом из четырех элементов, что следует из первой части описания a[4]. Каждый элемент a[i] этого массива в свою очередь является массивом из трех элементов типа int, что следует из второй части описания.

Для наглядности двумерный массив можно представить в виде таблицы с числом строк, равным первому размеру массива, и числом столбцов, равным второму размеру массива, например:

Массив а

Столбец 0

Столбец 1

Столбец 2

Строка 0

18

21

5

Строка 1

6

7

11

Строка 2

30

52

34

Строка 3

24

4

67

Имя двумерного массива без квадратных скобок за ним имеет значение адреса первого элемента этого массива, то есть значение адреса первой строки - одномерного массива из трех элементов. При использовании в выражениях тип имени двумерного массива преобразуется к типу адреса строки этого массива. В нашем примере тип имени массива a в выражениях будет приведен к типу адреса массива из трех элементов типа int и может использоваться во всех выражениях, где допускается использование соответствующего адреса.

Имя двумерного массива с одним индексным выражением в квадратных скобках за ним обозначает соответствующую строку двумерного массива и имеет значение адреса первого элемента этой строки. Например, в нашем случае a[2] является адресом величины типа int, а именно ячейки, в которой находится число 30, и может использоваться везде, где допускается использование адреса величины типа int.

Имя двумерного массива с двумя индексными выражениями в квадратных скобках за ним обозначает соответствующий элемент двумерного массива и имеет тот же тип. Например, в нашем примере a[2][1] является величиной типа int, а именно ячейкой, в которой находится число 52, и может использоваться везде, где допускается использование величины типа int.

В соответствии с интерпретацией описания двумерного массива (слева-направо) элементы последнего располагаются в памяти ЭВМ по строкам.

Инициализация двумерного массива также проводится по строкам, например, для того чтобы получить вышеописанный массив a, можно было бы провести следующую инициализацию

int a[][3] = {

{ 18, 21, 5 },

{ 6, 7, 11 },

{ 30, 52, 34 },

{ 24, 4, 67 }

};

Здесь первый размер массива будет определен компилятором. Следует отметить, что второй размер массива должен быть всегда указан. Это необходимо для того, чтобы сообщить компилятору размер строки массива, без которого компилятор не может правильно разместить двумерный массив в памяти ЭВМ.

Для инициализации двумерного массива символов можно использовать упрощенный синтаксис инициализации строк:

char s[][17] = {

"Строка 1",

"Длинная строка 2",

"Строка 3"

}

Размер памяти заказанный под каждую строку в этом случае должен быть равным длине самой длинной строки с учетом нуль-символа. При этом, для части строк (строка 1 и строка 3) будет выделено излишнее количество памяти. Таким образом, хранение строк различной длины в двумерном массиве символов недостаточно эффективно с точки зрения использования памяти.

Ввод двумерного массива осуществляется поэлементно с помощью двух вложенных циклов. Следующий фрагмент программы предназначен для ввода по строкам двумерного массива элементов типа double размером n строк на m столбцов

for (i=0; i<n; i++)

for (j=0; j<m; j++)

{

printf("a[%d][%d] = ", i, j);

scanf ("%lf", &a[i][j]);

}

Для ввода массива по столбцам достаточно поменять местами строки программы, являющиеся заголовками циклов.

Вывод такого же двумерного массива иллюстрирует следующий фрагмент:

for (i=0; i<n; i++)

{

for (j=0; j<m; j++) printf ("%9.3lf ", a[i][j]);

printf("\n");

}

В данном фрагменте после вывода очередной строки массива осуществляется переход на следующую строку дисплея.

В языке Си допускается использовать не только двумерные, но и трехмерные, четырехмерные и т. д. массивы. Их использование ничем принципиально не отличается от использования двумерных массивов, однако на практике они применяются значительно реже.

Пример обработки многомерного массива.

Дана матрица A(3,4). Найти сумму элементов в каждой строке.

#include <stdio. h>

#include <conio. h>

main ( )

{ int a[3] [4], sum [3], i, j; // описание матрицы и вектора

for ( i=0 ; i<3; i++) // ввод матрицы

for ( j=0; j<4; j++)

{printf (“\n Введите элемент a [%d][%d]=”, i, j);

scanf (“%d”,&a[i] [j];)

}

for ( i=0; i<3; i++)

{sum [ i ]=0;

for ( j=0; j<4; j++)

sum [i] = sum [i]+ a[i] [ j];

}

printf (“\n Вывод матрицы:\n);

for (i=0; i<3; i++) //форматный вывод матрицы по строкам

{ for (j=0; j<4; j++)

printf (“%4d”,a [i] [j] );

printf (“\n”); // переход на новую строку

}

printf (“\n Вывод массива сумм:\n);

for (j=0; j<3; j++)

printf (“%5d”,sum [ j ] ); // вывод массива sum

getch();

return 0;

}

В программе элементы матрицы вводятся по строкам по одному с подтверждением клавишей Enter. А выводятся в общепринятом виде: каждая строка матрицы с новой строки экрана (цикл i по строкам внешний, а цикл j – внутренний). Если поменять местами циклы, то будет вывод матрицы по столбцам.

9.6.  Адресная арифметика языка Си

Язык Си имеет средства работы непосредственно с областями оперативной памяти ЭВМ, задаваемыми их адресами (указателями). В языке C указатели строго типизированы, т. е. различают указатели (адреса) символьных, целых, вещественных величин, а также типов данных, создаваемых программистом.

Для описания указателя на какой-либо тип данных перед именем переменной ставится *. Например в строке

int *a, *b, c, d;

описываются два адреса и две переменные целого типа. В строке

double *bc;

описан адрес переменной вещественного типа. Никогда не следует писать знак * слитно с типом данных, например как в следующей строке:

int* a, b;

В этой строке создается ложное впечатление о том, что описаны два указателя на тип int, в то время как на самом деле описан один указатель на int, а именно a, и одна переменная типа int.

Описание переменных заставляет компилятор выделять память для хранения этих переменных. Описание указателя выделяет память лишь для хранения адреса. В этом смысле указатель на целое данное и на тип double будут занимать в ЭВМ одинаковое количество байт памяти, зависящее от модели памяти, на которую настроен компилятор. Например, в 16-ти разрядной Small модели длина указателя равна двум байтам, а в 16-ти разрядной Large модели - четырем.

При описании указателей в качестве имени типа данных можно использовать ключевое слово void, например

void *vd;

При таком описании с указателем не связывается никакой тип данных, то есть получаем указатель на данное произвольного типа.

Для указателей одного и того же типа допустимой является операция присваивания, кроме того указателю типа void может быть присвоено значение адреса данного любого типа, но не наоборот, например

int *a, *b;

double *d;

void *v;

...

a = b; /* Правильно */

v = a; /* Правильно */

v = d; /* Правильно */

b = v; /* Неправильно */

d = a; /* Неправильно */

В случае неправильного присваивания указателей компиляторы обычно выдают предупреждающие сообщения, которыми никогда не следует пренебрегать. Например, компилятор фирмы Borland выдает сообщение:

"Suspicious pointer conversion", которое переводится как "Подозрительное преобразование указателей".

Если по какой-либо причине необходимо выполнить операцию присваивания между указателями разного типа, то следует использовать явное преобразование типов, например для указателей из предыдущего примера

b = (int *) v;

d = (double *) a;

При этом ответственность за корректность подобных операций целиком ложится на программиста. Действительно, в предыдущем примере a является указателем на ячейку памяти для хранения величины типа int. Обычно это ячейка размером 2 байта. После присваивания указателей с явным преобразования типов, делается возможным обращение к этой ячейке посредством указателя d, как к ячейке с величиной типа double. Размер этого типа обычно 8 байт, да и внутреннее представление данных в корне отличается от типа int. Никакого преобразования самих данных не делается, ведь речь идет только об указателях. Дальнейшая работа с указателем d скорее всего заденет байты, соседние с байтами на которые указывает a. Результат интерпретации этих байт будет тоже неверным.

Для поддержки адресной арифметики в языке Си имеются две специальные операции - операция взятия адреса & и операция получения значения по заданному адресу * (операция разадресации).

Операция & может применяться к любому объекту программы, адрес которого в принципе может быть определен. Результатом операции является указатель того же типа, что и тип объекта, имеющий значение адреса объекта. Если эту операцию применить к указателю, то результатом будет адрес ячейки памяти, в которой хранится значение указателя. Результат операции & можно использовать в любом выражении, где допускается использование указателя соответствующего типа.

Операция * может применяться только к указателям и только в том случае когда они типизированы. При необходимости применить эту операцию к указателю типа void следует использовать явное преобразование типов. Результатом операции * является значение того объекта, к адресу которого применялась операция *, тип результата совпадает с типом объекта. Результат операции * можно использовать в любом выражении, где допускается использование объекта соответствующего типа.

Рассмотрим работу вышеописанных операций на следующем примере

int *p, a, b;

double d;

void *pd;

p = &a;

*p = 12;

p = &b;

*p = 20;

/* Здесь a содержит число 12, b - число 20 */

pd = &d;

*( (double *) pd ) = a;

/* Здесь d содержит число 12.0 */

Поясним первые четыре присваивания рисунком, в котором прямоугольники изображают ячейки памяти для хранения величин типа int и указателей, внутри которых проставлены значения величин, а над ними записаны их названия и адреса.

Состояние ячеек до первого присваивания

P, адрес 1000

a, адрес 2000

b, адрес 4000

мусор

мусор

мусор

Состояние ячеек после присваивания p = &a

p, адрес 1000

a, адрес 2000

b, адрес 4000

2000

мусор

мусор

Состояние ячеек после присваивания *p = 12

p, адрес 1000

a, адрес 2000

b, адрес 4000

2000

12

мусор

Состояние ячеек после присваивания p = &b

p, адрес 1000

a, адрес 2000

b, адрес 4000

4000

12

мусор

Состояние ячеек после присваивания *p = 20

p, адрес 1000

a, адрес 2000

b, адрес 4000

4000

12

20

Описание указателя не является требованием на выделение памяти для хранения данных. Память выделяется только для хранения адреса. Поэтому прежде, чем использовать указатель, ему нужно присвоить значение адреса реально существующего объекта. В противном случае результат работы программы непредсказуем. Рассмотрим, например, следующую последовательность строк

double *a, b;

b = *a;

*a = 135.7;

В этой последовательности используется указатель, которому предварительно не присвоено никакого значения. В ячейке a находится произвольное значение, возможно оставшееся от работы предыдущей программы. Первая операция присваивания приведет к тому, что переменная b получит значение из ячейки памяти с непредсказуемым адресом. Вторая - к тому, что по непредсказуемому адресу будут записаны 8 байт, являющиеся двоичным представлением числа 135.7. Если эти байты попадут на область данных программы, то программа скорее всего выдаст неправильный результат. Если они попадут на область кода программы или на системную область MS DOS, то в лучшем случае программа аварийно завершится, а в худшем компьютер полностью зависнет.

Если делается попытка присвоить какое-либо значение по адресу указателя, значение которого равно нулю, то многие компиляторы выдают сообщение

Null pointer assingment

К сожалению, это сообщение выдается уже после того, как программа завершилась, если она смогла завершиться. Для компилятора фирмы Borland легко можно отследить момент некорректного обращения к памяти. Для этого нужно в окно просмотра значений выражений поместить выражение (char *) 4, затем пошагово выполнять программу до тех пор, пока строка-подпись фирмы Borland в окне просмотра не изменится. Если программа имеет большой размер, то более целесообразно выполнять ее от одной точки останова до другой.

Следует также опасаться случая, когда указатель содержит адрес объекта программы, завершившего свое существование. Например, результат работы следующей программы неверен и непредсказуем:

#include <stdio. h>

#include <math. h>

double * Cube(double x)

{

double cube_val;

cube_val = x*x*x;

return &cube_val;

}

void main(void)

{

double *py;

py = Cube(5);

printf("y1 = %lf\n", *py);

sin(0.7);

printf("y1 = %lf\n", *py);

}

Это происходит потому, что функция Cube() возвращает указатель на локальную переменную cube_val, которая существует только в пределах функции Cube(). После возврата из функции память, ранее распределенная под переменную cube_val, освобождается и может быть использована компилятором для других целей несмотря на то, что значение возвращенного указателя по-прежнему содержит ее адрес.

9.7.  Указатели и одномерные массивы

В языке C понятие массива тесно связано с понятием указателя. Действительно, как было описано выше, имя массива представляет собой адрес области памяти, распределенной под этот массив, или иными словами адрес первого элемента массива. Пусть описаны следующие данные:

int a[100], *pa;

и осуществлено присваивание:

pa = a;

Оно является корректным, поскольку имя a обозначает адрес первого элемента массива a и поэтому имеет тип указателя на int. После этого присваивания

pa[0] или *pa будет обозначать a[0];

pa[1] или *(pa+1) будет обозначать a[1];

pa[2] или *(pa+2) будет обозначать a[2] и т. д. И вообще обозначения вида *(pa+n) и pa[n] являются полностью эквивалентными. Точно также эквивалентны выражения *(a+i) и a[i].

На первый взгляд кажется, что массив и указатель полностью эквивалентны. Однако имеется два существенных отличия массива от указателя:

-  массиву при описании выделяется память для хранения всех его элементов, а указателю только для хранения адреса;

-  адрес массива навсегда закреплен за именем, то есть имя массива является адресной константой и выражение вида a = pa недопустимо.

Результат прибавления к указателю или вычитания из него целочисленной величины является указателем того же типа, значение которого отличается от значения исходного указателя на число байт, определяемое произведением целочисленной величины на размер данного, которое адресует указатель. Например, если имеются описания

int A[20], *pA = A;

double B[20], *pB = B;

то указатель (pA+3) будет иметь значение на 6 байт больше, чем pA, и будет адресовать элемент A[3] массива A. Указатель (pB+3) будет иметь значение на 24 байта больше, чем pB, и будет адресовать элемент B[3] массива B. С указателями типа void подобные операции выполнены быть не могут, поскольку компилятор не знает размера адресуемого данного.

Для указателей определены операции увеличения и уменьшения на целочисленную величину, как альтернативная форма записи выражений

pA = pA + i; эквивалентно pA += i;

pA = pA - i; эквивалентно pA -= i;

pA = pA + 1; эквивалентно pA++; или ++pA;

pA = pA - 1; эквивалентно pA--; или --pA; При этом, работа префиксных и постфиксных операций ++ и -- совпадает с их работой для арифметических данных.

Указатели допускается использовать в операциях сравнения. При этом всегда возможно сравнение указателя с нулем и сравнение двух однотипных указателей. Однако правильность результата последнего сравнения для 16-ти разрядного режима работы IBM PC гарантируется только в том случае, если сравниваемые указатели являются указателями на элементы одного и того же массива данных или если они предварительно подвергаются нормализации (см. ниже).

Разность двух однотипных указателей представляет собой целочисленную величину равную количеству элементов данных (не байт) между соответствующими адресами памяти. Правильность результата этой операции для 16-ти разрядного режима работы IBM PC тоже гарантируется только в том случае, если указатели имеют значение адресов элементов одного и того же массива данных или если они предварительно подвергаются нормализации (см. ниже).

В следующем фрагменте программы иллюстрируется использование вышеописанных операций

double A[100], *pA, *pA100;

int i;

/* Заполняем массив A. Работаем с массивом */

for (i=0; i<100; i++) A[i]=0;

/* Заполняем массив A. Работаем с указателями */

for (pA=A, pA100=pA+100; pA<pA100; pA++) *pA=11.9;

Последний вариант заполнения массива может оказаться более эффективным.

9.8.  Указатели и двумерные массивы

Пусть имеются следующие определения массивов и указателей:

int A[4][2], B[2];

int *p, (*pA)[4][2], (*pAstr)[2];

Здесь A представляет собой двумерный массив из четырех строк и двух столбцов, B - одномерный массив из двух элементов. Для каждого из этих массивов будет выделено соответствующее количество памяти, достаточное для хранения всех их элементов.

Указатель p представляет собой указатель на величину int, указатель pA - указатель на двумерный массив из четырех строк и двух столбцов, pAstr - указатель на одномерный массив из двух элементов. Все указатели имеют размер, равный размеру адреса для данных в используемой модели памяти. Память для хранения данных, естественно, не выделяется. Количество элементов данных из описания массивов будет использовано лишь для корректного изменения значения указателя при выполнении над ним допустимых арифметических операций.

Смысл трактовки этих указателей определяется направлением слева-направо для подряд следующих операций [], а также изменением приоритета операции * с помощью круглых скобок. Если не поставить круглых скобок, то следующее определение

int *pa[4][2];

рассматривается как определение двумерного массива из указателей на тип int.

Для вышеописанных указателей допустимы следующие операции присваивания, поскольку слева и справа от операции присваивания находятся указатели на один и тот же тип данных:

p = B;

p = &B[1];

p = &A[0][0];

p = A[2];

Следующее присваивание:

p = A; /* неверно */

является неверным, так как слева от операции присваивания находится указатель на тип int, а справа - указатель на первый элемент массива A, который (элемент) представляет собой массив из двух элементов типа int. В таких случаях компиляторы выдают предупреждающее сообщение о подозрительном преобразовании указателя.

Если программист уверен в своих действия, то он может использовать операцию явного приведения типа для устранения этого сообщения, но при этом компилятор снимает с себя всякую ответственность за корректность использования такого указателя. Так, после присваивания

p = (int *) A;

элементы, на которые ссылается указатель, и элементы массива A находятся в следующем соответствии:

p[0] эквивалентно A[0][0]

p[1] эквивалентно A[0][1]

p[2] эквивалентно A[1][0]

p[3] эквивалентно A[1][1]

p[4] эквивалентно A[2][0]

p[5] эквивалентно A[2][1]

p[6] эквивалентно A[3][0]

p[7] эквивалентно A[3][1]

Совершенно корректными являются следующие присваивания

pAstr = A;

после которого использование массива A и указателя pAstr совершенно эквивалентны:

pAstr[i][j] эквивалентно A[i][j]

Присваивание

pAstr = &A[2];

устанавливает следующее соответствие между элементами, на которые ссылается указатель pAstr и элементами массива A:

pAstr[0][0] эквивалентно A[2][0]

pAstr[0][1] эквивалентно A[2][1]

pAstr[1][0] эквивалентно A[3][0]

pAstr[1][1] эквивалентно A[3][1]

Следующие присваивания корректны

pA = &A; /* Указатель на двумерный массив */

pAstr = &B; /* Указатель на одномерный массив */

и устанавливают следующее соответствие элементов:

(*pA)[i][j] эквивалентно A[i][j]

(*pAstr)[i] эквивалентно B[i]

Массивы указателей удобны для хранения символьных строк:

char *str[] = {

"Строка 1",

"Строка 2",

"Длинная строка 3"

};

В этом случае каждый элемент массива представляет собой адрес соответствующей строки символов, а сами строки располагаются компилятором в статическом сегменте данных. Никакой лишней памяти, связанной с различной длиной строк, при этом не расходуется.

9.9.  Указатели и функции

Функции, как и другие объекты программы, располагаются в памяти ЭВМ. Любая область памяти имеет адрес, в том числе и та, в которой находятся функция. Имя функции без круглых скобок за ним представляет собой константный адрес этой области памяти. Таким образом, имея функции со следующими прототипами:

double sin(double x);

double cos(double x);

double tan(double x);

мы можем в программе использовать имена sin, cos и tan, которые будут обозначать адреса этих функций.

Можно описать и указатель на функцию. Например, для функции с аргументом типа double, возвращающей значение типа double, описание такого указателя будет выглядеть следующим образом:

double (*fn)(double x);

Здесь, как и в случае указателя на массив, круглые скобки увеличивают приоритет операции *. Если бы они отсутствовали, то была бы описан не указатель на функцию, а функция, возвращающая значение указателя на double.

Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7