Fseek(указатель на файл, позиция, параметр)




где позиция – количество байтов (символов), отстоящих от заданного места, параметрSEEK_SET – начало файла,

SEEK_CUR – текущая позиция файла,

SEEK_END – конец файла.

Пример 1: прочитать символ из бинарного файла, отстоящий от начала файла на 9 байтов:

f_bin=fopen("D:/User/my_file.dat", "rb");

fseek(f_bin, 9, SEEK_SET);

fscanf(f_bin, "%c", &d);

printf("%c", d);

На экран будет выведено:

*

Пример 2: прочитать символ из бинарного файла, отстоящий от конца файла на 6 байтов:

f_bin=fopen("D:/User/my_file.dat", "rb");

fseek(f_bin, -6, SEEK_END);

fscanf(f_bin, "%c", &d);

printf("%c", d);

На экран будет выведено:

+

б) определение номера текущей позиции, занимаемой указателем файла (нумерация позиций начинается с нуля)

fgetpos(указатель на файл, fpos_t *pos)

где pos – текущая позиция указателя файла – переменная типа fpos_t (определен в stdio.h).

Пример: определить номера позиций начального и конечного символов файла:

fpos_t *nomer;

f_bin=fopen("D:/User/my_file.dat", "rb");

fgetpos(f_bin, &nomer); // номер начального символа

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

while (!feof(f_bin)) // читаем файл до конца

{

fscanf(f_bin, "%c", &d);

}

fgetpos(f_bin, &nomer); // номер конечного символа

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

На экран будет выведено:

nomer=0

nomer=15


Функции

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

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

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

Примерами подпрограмм являются программы вычисления отдельных функций, имеющихся в библиотеках языков программирования, – стандартных функций, а также функций ввода-вывода. Эти подпрограммы размещены в системной библиотеке языка, подключаемой к программе пользователя при ее обработке. Значит, используя в своей программе, например, функцию printf, мы обращаемся к стандартной библиотеке ввода-вывода stdio.h, подсоединяемой к нашей программе директивой препроцессора #include <stdio.h>.

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

В отличие от Паскаля в языке Си используется только один вид подпрограмм – функции. Вообще любая программа на Си представляет собой совокупность взаимосвязанных функций, одна из которых является основной (головной) – main. С нее и начинается выполнение программы.

Функция – это подпрограмма, состоящая из заголовка, описания локальных (внутренних) переменных и блока (операторов, реализующих алгоритм вычислений). Она предназначена для вычисления единственного значения, зависящего от нескольких аргументов (формальных параметров).

Один из недостатков Паскаля заключался в том, что описание используемой функции нужно было помещать внутрь вызывающей ее программы, в раздел описания процедур и функций. В языке Си описание функций помещается отдельно от вызывающих программ, а перед заголовком main() основной программы помещаются прототипы (объявления) этих функций:

Директивы препроцессора

Прототипы функций

Void main()

{

Головная программа с обращениями к функциям

}

Описание функций

Таким образом, функции, используемые в программе, должны быть обязательно объявлены – указан их прототип. Прототип – это заголовок функции с указанием ее типа, имени, типов и имен аргументов – формальных параметров:

int summa(int a, int b);

void vorm_mass(int n, int m, int mass[n][m]);

Внимание! После закрывающих скобок точка с запятой ставится обязательно!

Таким образом, прототип функции полностью соответствует ее заголовку, используемому при ее дальнейшем описании.

Сами функции описываются после головной программы:

Int summa(int a, int b)

{

int s; // s – локальная переменная

s = a + b;

return s; // возврат вычисленного значения

}

void vorm_mass(int n, int m, int mass[n][m])

{

int i, j;

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

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

{

// задание значений элементам массива mass[n][m]

}

}

Внимание! После заголовка функции и закрывающих фигурных скобок точка с запятой не ставится!

В последнем случае оператор return не нужен, так как функция vorm_mass имеет тип void – ничего не возвращает.

Если функция не имеет формальных параметров, то их в заголовке функции
не указывают, но круглые скобки оставляют.

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

Взаимное расположение функций в программе может быть произвольным. Однако следует избегать обращения к функции, еще не объявленной или не описанной, так как такая функция для компилятора всегда будет иметь тип int.

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

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

Int summa(int a, int b)

{

return a + b; // возврат вычисленного значения

}

Если после слова return ничего не стоит или этого слова вообще нет
в функции, то значит, что данная функция не возвращает никакого значения, и поэтому в ее заголовке должен быть указан тип void.

Формальные параметры функции в Си соответствуют параметрам-значениям подпрограмм в Паскале: они служат только для передачи значений фактических параметров в функцию и вычисления ее значения. Поэтому даже если внутри функции они изменяются, то соответствующие им фактические (входные) параметры не изменяются, оставаясь такими же, какими они были при вызове функции: с чем пришел – с тем
и ушел
:

Int summa(int a, int b)

{

int s; // s – локальная переменная

a++;

b++;

s = a + b;

return s; // возврат вычисленного значения

}

Внутри тела функции формальные параметры a и b увеличились на единицу, и сумма определяется при их новых значениях. При выходе из этой функции фактические (входные) параметры, соответствующие этим формальным параметрам, не изменятся.

Таким образом, функция в Си вычисляет единственное значение, передаваемое вовне оператором return.

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

Создадим функцию, которая обменивает значениями две переменных:

Void swap(int a, int b)

{

int temp; // temp – локальная переменная

temp=a; // алгоритм циклического обмена

a=b;

b=temp;

}

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

void swap(int *a, int *b) // используем значения переменных

{ // по адресам переменных a и b

int temp;

temp=*a; // алгоритм циклического обмена

*a=*b; // значениями, находящимися

*b=temp; // по этим адресам

}

Эта функция использует не формальные параметры, а значения, находящиеся по адресам формальных параметров. Сами адреса переменных функцией не изменяются, как это и положено формальным параметрам. Меняются только значения, находящиеся по данным адресам, а эти значения не являются формальными параметрами. Хитро придумано!

Обратимся к этой функции, например, из головной программы:

int x, y;

x = 5;

y = 3;

swap(&x, &y); // используем адреса фактических переменных

В этом случае переменные x и y обменяются своими значениями.

Если в качестве формальных параметров используются имена массивов (строк), то в списке фактических параметров перед ними знаки амперсанда &не ставятся: имя массива в Си является адресом его первого элемента. Поэтому в функцию передается не массив со всеми значениями его элементов, а только адрес его первого элемента. Адреса всех остальных элементов вычисляются автоматически:

void poplavok(int n, int vector[n])

{

int top, bottom, temp;

for (top=0, bottom = n-1; top<bottom; top++, bottom--)

{

temp = vector[top];

vector[top] = vector[bottom];

vector[bottom] = temp;

}

}

Эта функция переворачивает вектор vector[n] – выполняет «поплавок ».

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

void poplavok(int n, int vector[])

Обратимся к этой функции из головной программы:

#include <stdio.h>

#include <conio.h>

void poplavok(int n, int vector[]); // прототип функции

Int main()

{

int i, k=5;

int vect[k] = {1,2,3,4,5}; // инициализация вектора

printf("\n"); // вывод исходного вектора

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

printf("%5d", vect[i]);

printf("\n");

poplavok(k, vect); // обращение к функции

for (i=0; i<k; i++) // вывод полученного вектора

printf("%5d", vect[i]);

printf("\n");

printf("\n");

}

void poplavok(int n, int vector[]) // описание функции

{

int top, bottom, temp;

for (top=0, bottom = n-1; top<bottom; top++, bottom--)

{

temp = vector[top];

vector[top] = vector[bottom];

vector[bottom] = temp;

}

}

На экран будет выведено:

1 2 3 4 5

5 4 3 2 1

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

void vorm_mass(int n, int m, int mass[n][m])

{

int i, j;

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

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

{

mass[i][j]=i + j;

}

}

Обратимся к этой функции из головной программы:

#include <stdio.h>

#include <conio.h>

void vorm_mass(int n, int m, int mass[n][m]); // прототип

Int main()

{

int i, j;

int k=2, d=3;

int massiv[k][d];

vorm_mass(k, d, massiv); // обращение к функции

printf("\n");

for (i=0; i<k; i++) // вывод полученного массива

{ // построчно

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

printf("%5d", massiv[i][j]);

printf("\n");

}

}

void vorm_mass(int n, int m, int mass[n][m])

{ // описание функции

int i, j;

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

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

{

mass[i][j]=i + j;

}

}

Результат работы программы:

0 1 2

1 2 3

 

Рекурсия

Использование рекурсии в программировании базируется на рекурсивных математических определениях. Считается, что в математике рекурсивность как принцип определений используется с 1890 года. Впервые ее применил Д. Гильберт.

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

Например, вычисление факториала целого неотрицательного числа n! = 1·2·3·…·(n-1) · n. Кроме того, по определению, 0! = 1. Рекурсивное математическое определение факториала имеет вид

1 при n = 0,

n!=(n – 1)!·n при n > 0.

 

Последовательность чисел Фибоначчи имеет вид 1, 1, 2, 3, 5, 8, 13…

В ней два первых числа фиксированы и равны единице, а каждое последующее число равно сумме двух предыдущих. Рекурсивное математическое определение числа Фибоначчи с порядковым номером n имеет вид:

1 при n = 1,

1 при n = 2,

Fn=Fn-2 + Fn-1 при n > 2.

 

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

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

Рассмотрим последовательность факториалов целых чисел 0!, 1!, 2!, 3!, …, в которой ai = i!, i = 1, 2, 3, …. Эту же последовательность можно представить в виде рекуррентной формулы: ai = ai-1·i, a0 = 1, i = 1, 2, 3… Эта формула задает последовательность, в которой каждый очередной член зависит непосредственно от предшествующего. Начальный член последовательности a0 задан прямою. Найдя член последовательности с порядковым номером i = n, мы тем самым решим задачу вычисления n!

Рекуррентная формула для вычисления числа Фибоначчи с заданным порядковым номером i = n практически не отличается от рекурсивного определения :

Fi = Fi-2 + Fi-1, F1 = 1, F2 = 1, i = 3, 4, 5,…

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

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

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

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

При рекурсивном обращении каждый раз приходится запоминать не только адрес возврата, но и всю совокупность данных вызывающей подпрограммы (локальные переменные и параметры-значения). С этой целью используется автоматически выделяемая область памяти – стек, структура, работающая по принципу LIFO (Last in – first out: последним пришел – первым вышел). Такой метод работы с памятью обеспечивает строгое соответствие прямого порядка записи данных обратному порядку их чтения. Только с помощью стека можно достаточно просто обеспечить корректное завершение работы цепочки подпрограмм, каждая из которых вызывает следующую: сначала должна быть завершена последняя, затем – предпоследняя и так далее. Максимальный размер стека – 65520 байт. Поэтому последовательность рекурсивных обращений не может быть бесконечной. В любой рекурсивной подпрограмме должна быть нерекурсивная (терминальная) ветвь, обеспечивающая выход из рекурсии. При переполнении стека работа программы прерывается и появляется сообщение об ошибке.

Рекурсивная функция, вычисляющая факториал заданного числа n, может иметь вид:

Long factorial(int n)

{

if (n <= 1)

return 1; // выход из рекурсии – терминальная ветвь

else return n * factorial(n-1);

}

При n = 5 эта функция будет работать следующим образом:

factorial:= 5 * factorial(4)

5 * 4 * factorial(3)

5 * 4 * 3 * factorial(2)

5 * 4 * 3 * 2 * factorial(1)

5 * 4 * 3 * 2 * 1 = 120

В данном случае реализована так называемая нисходящая рекурсия: вызов factorial(5) означает, что функция factorial вызывает себя раз за разом: factorial(4), factorial(3), … – до тех пор, пока не будет достигнута терминальная ситуация – ситуация окончания рекурсии. При каждом вызове текущие вычисления откладываются, локальные переменные и адрес возврата остаются в стеке. Терминальная ситуация factorial = 1 достигается при n = 1. При этом рекурсивный спуск заканчивается, начинается рекурсивный возврат изо всех вызванных в данный момент копий функции: начинает строиться ответ n*factorial(n-1). Сохраненные локальные параметры выбираются из стека в обратной последовательности, а получаемые промежуточные результаты: 1*1, 2*1, 3*2*1, 4*3*2*1, 5*4*3*2*1 – передаются вызывающим функциям.

Рекурсивная функция, вычисляющая n- й член ряда Фибоначчи, может иметь вид:

Int fibo(int n)

{

if ((n == 1) || (n == 2))

return 1; // выход из рекурсии

else return fibo(n-2) + fibo(n-1);

}

Примеры

1. Составить функцию, рекурсивно определяющую значение биномиального коэффициента при 0<m<n по формулам:

= = 1, = +

Int binom(int m, int n)

{

if ((m == 0) || (m == n))

return 1; // выход из рекурсии

else return binom(m, n-1) + binom(m-1, n-1);

}

2. Составить функцию, рекурсивно определяющую максимальный элемент в заданной части целочисленного массива vectorn, начиная с k -го и до n -го элемента:

int max_element(int k, int n, int vector[])

{

int temp;

if (k == n-1)

return a[n-1]

Else

{

temp = max_element(k+1, n, vector[]);

if (a[k] > temp)

return a[k];

else return temp;

}

}

3. Составить функцию, реализующую рекурсивный алгоритм К. Хоара быстрой сортировки массива vectorn. Сравниваются элементы vectori и vectorj, причем i = 1, j = n-1. Если vectori< vectorj, то эти элементы уже отсортированы по возрастанию, поэтому значение правого индекса уменьшается на единицу, и алгоритм повторяется. Если vectori> vectorj, то они меняются местами, останавливается правый индекс и начинает увеличиваться левый. Обмен значениями с изменением направления движения после каждого обмена продолжается до тех пор, пока левый и правый индексы не встретятся друг с другом: i = j. В этом случае элемент vectori будет стоять на своем месте в массиве: слева от него стоят элементы меньше его, а справа – больше. После этого алгоритм рекурсивно повторяется для левой и правой частей массива:

void quick_sort(int left, int right, int vector[])

{

int i, last;

if (left >= right) // в векторе меньше двух элементов

return;

swap(left, (left + right)/2, vector);

last= left;

for (i=left+1; i<=right; i++)

if (vector[i]<vector[left])

swap(++last, i, vector);

swap(left, last, vector);

quick_sort(left, last-1, vector);

quick_sort(last+1, right, vector);

}

Операцию перестановки i -го и j -го элементов массива можно оформить функцией

void swap(int i, int j, int vector[])

{

int temp;

temp=vector[i];

vector[i]=vector[j];

vector[j]=temp;

}

Особенности рекурсии:

· использование рекурсивной формы организации алгоритма выглядит изящнее итерационной и дает более компактный текст программы,

· недостатки рекурсии состоят в следующем:

1) если глубина рекурсии велика, то программа будет требовать во время исполнения много памяти, что может привести к переполнению стека,

2) рекурсивные алгоритмы, как правило, выполняются более медленно,

3) при рекурсивном программировании велика вероятность ошибок, вынуждающих программиста к перезагрузке компьютера.

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

АДРЕСА И УКАЗАТЕЛИ

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

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

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

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

Язык Си отличается от других языков программирования прежде всего широким использованием указателей. Именно наличие в нем указателей сделало его очень удобным для системного программирования.

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

Оперативная память компьютера – это последовательность байтов, каждый из которых имеет номер (адрес). Адреса байтов памяти идут по возрастанию: если к – это номер текущего байта, то к - 1 – номер предыдущего, а к + 1 – последующего байтов. Указатель как раз и содержит адрес некоторого байта памяти. Меняя значение указателя, можно перемещаться по памяти, адресуясь к различным данным, записанным в просматриваемых ячейках.

Таким образом, в Си под указателем понимается переменная, содержащая адрес любого объекта программы. Значит, указатель говорит о том, где в памяти размещен тот или иной программный объект (переменная, массив, структура, функция), но ничего не говорит об имени и значении этого объекта. По аналогии – можно знать почтовый адрес человека, но этот адрес не дает нам информацию о количестве комнат в его квартире и ее обстановке.

Применение указателей полезно:

- при работе с массивами – обеспечивается использование сразу всех элементов массива,

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

- при работе с файлами – обеспечивается быстрый доступ к компонентам файла,

- при создании новых переменных в процессе выполнения программы – можно динамически выделять память для них.

Для работы с указателями в Си используются две операции:

* – доступ по адресу (обращение по адресу),

& – получение адреса.

Знак *, стоящий перед именем переменной-указателя, означает «взять (записать) значение по данному адресу»: *ptr – записать значение по адресу ptr.

Знак &, стоящий перед именем обычной переменной, означает «получить адрес переменной»: &x – получить адрес переменной x.

Перед использованием в программе указатели, как и любые другие переменные, должны быть описаны – задан тип объекта, адрес которого будет хранить указатель, поставлена звездочка * и задано имя указателя:

int i, j, *ptr;

float x, y, *ukaz;

char c, d, *adr;

Описаны указатели:

рtr – на любой объект (переменную, массив, функцию) целого типа,

ukaz – на любой объект вещественного типа,

adr – на любой объект символьного типа.

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

ptr = &i;

ukaz = &x;

adr = &c;

Сейчас указатели ptr, ukaz, adr будут хранить адреса (номера) первых (младших) байтов памяти, отведенной для переменных i, x, c (адрес байта памяти – это шестнадцатеричное число).

Присвоим этим переменным некоторые значения:

i = 5;

x = 3.1416;

c = ‘@’;

Обратимся к ним по адресу:

j = *ptr;

y = *ukaz;

d = *adr;

Содержимое ячейки памяти с адресом ptr (а там записано число 5 – ведь это адрес переменной i) будет присвоено переменной j, содержимое ячейки памяти с адресом ukaz – переменной y, содержимое ячейки памяти с адресом adr – переменной d.

Таким образом, два оператора присваивания:

ptr = &i;

j = *ptr;

выполняют то же самое, что и один оператор:

j = i;

Значит, можно организовать не только прямую передачу данных от одной переменной к другой (j = i), но и косвенную – через адреса переменных, даже не упоминая их имен. Указатели позволяют обращаться к конкретным ячейкам памяти и изменять их содержимое, не интересуясь тем, значения каких именно переменных хранятся в этих ячейках:

*adr = ‘+’;

переменной по адресу adr присвоено значение символа ‘+’,

(*ptr)++;

значение переменной по адресу ptr увеличивается на единицу,

(*ptr) += 3;

значение переменной по адресу ptr увеличивается на 3,

j = (*ptr)++;

значение переменной по адресу ptr присваивается переменной j и после этого увеличивается на единицу.

Если это выражение записать без скобок:

j = *ptr++;

то сначала значение адреса ptr увеличивается на единицу (получаем адрес следующей ячейки памяти), а затем содержимое новой ячейки памяти присваивается переменной j: операции доступа по адресу * и инкремента ++ имеют одинаковый приоритет, но выполняются справа налево.

 



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2017-03-31 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: