Главная страница

Лекции. Основные понятия и определения


Скачать 1.94 Mb.
НазваниеОсновные понятия и определения
Дата27.03.2018
Размер1.94 Mb.
Формат файлаdocx
Имя файлаЛекции.docx
ТипКонтрольные вопросы
#39570
страница20 из 58
1   ...   16   17   18   19   20   21   22   23   ...   58

7.3. Процедуры


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

В различных языках программирования допускаются внешние и внутренние процедуры. Внешняя процедура не содержится внутри любой другой процедуры, может вызываться из произвольных мест программы и транслироваться отдельно, независимо от других фрагментов программы. Это дает возможность разбить решаемую задачу на логические, по возможности, слабо связанные между собой части и вести их разработку независимо разными людьми. Передача информации между процедурами осуществляется путем передачи аргументов из вызывающей процедуры в вызываемую. В вызываемой процедуре каждому аргументу в порядке их передачи соответствует параметр, тип и организация которого должны совпадать с типом и организацией аргумента. После передачи аргументов в теле процедуры выполняются задуманные действия и результат этих действий может быть передан обратно в место вызова с помощью того же механизма. Этот результат включается в список параметров вызываемой процедуры (следовательно и в список аргументов вызывающей). Иногда для описания этого механизма употребляют термины фактический параметр (вызывающая процедура) и формальный параметр (вызываемая). Внутренняя процедура содержится внутри некоторой другой процедуры и на ее употребление накладываются существенные ограничения. В языках C и Basic допустимы только внешние процедуры.

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

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

Формально в языке нет понятия подпрограмма, определено лишь понятие функция. Однако последняя легко преобразуется в подпрограмму и для ее вызова применяется инструкция вида:

<имя_процедуры>([список_аргументов]);

Пример. Подпрограмма умножения матриц.

MultMatrix(a, b, c, m, l, n);

Здесь:

a – левая матрица,

b – правая матрица,

c – матрица-результат,

m – число строк левой матрицы и результата,

l – число столбцов левой=числу строк правой,

n – число столбцов правой и результата.

Basic

Вызов аналогичен языку C:

[Call]<имя_подпрограммы>([список_аргументов])

Пример. Подпрограмма умножения матриц.

Call MultMatrix(a, b, c, m, l, n ) или MultMatrix(a, b, c, m,l, n)

Употребление ключевого слова call необязательно.

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

Пример вызова.

Basic: y=Math.Sqrt(x*x+z*z)+Math.Abs(x-z)

C: y=sqrt(x*x+z*z)+fabs(x-z);

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

Пример. Функция printf(<управляющая строка>[, <список аргументов>);

<управляющая строка> является первым обязательным параметром.

7.3.1. Определения процедур


C

Формат функции:

[<тип>] <имя>([<описания_параметров>])<блок>

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

Примеры.

float MinFun(int n, float x[ ]){

int i, j;

float MinValue;

................

return 2*MinValue+5;

}

Определение функции: Вызов:

double Max(double a, double b){ y=Max(cos(x), sin(x));

double y;

if(a>b){

y=a;

}else{

y=b;

}

return y;

}

Basic

Формат функции:

Function <имя_функции>([<описания_параметров>]) As <тип>

блок

End Function

Возвращаемое значение непосредственно присваивается имени функции, т.е. в блоке должна присутствовать хотя бы 1 инструкция вида <имя_функции> = <выражение> либо возвращается с помощью инструкции Return <выражение>.

Примеры.

Function Minfun(n As Integer, x( ) As Single) As Single

Dim i, j As Integer, MinValue, y As Single

................................................................................

MinValue=2*y+5 | Return 2*y+5

End Function

Определение функции Вызов

Function Max(a As Double, b As Double) As Double y=Max(Cos(x), Sin(x));

Dim y As Double;

If a>b Then

y=a

Else

y=b

End If

Max=y

End Function

C

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

Пример.

void fun( ){

………….

[return;]

........……

}

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

Пример(подпрограмма). Вызов:

/* Умножение матрицы на вектор */ Matrix(m, n, a, b, c);

void Matrix (int l, // Число строк

int k, // Число столбцов

double p[ ][10], // Исходная матрица

double q[ ], // Исходный вектор

double s[ ] ){ // Вектор-результат

int i, j;

for(i=0; i

for(s[ i ]=j=0; j

s[ i ]+=p[ i ][ j ]*q[ j ];

}

}

} // End Matrix

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

int main([<параметры>])<блок>

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

Параметры используются редко, поэтому обычно имеем:

int main( ){

............

}

Basic

Формат подпрограммы:

Sub <имя_подпрограммы>([<описания_параметров>])

..................................

Exit Sub|Return

..................................

End Sub

Для возвращения в точку вызова используются инструкции Exit Sub | Return.Если этопроизводится в конце процедуры, то их употреблять ненужно.

Пример (подпрограмма). Вызов:

' Умножение матрицы на вектор [ Call] Matrix(m, n, a, b, c);

Sub Matrix( l As Integer, _

k As Integer, _

p( , ) As Double, _

q( ) As Double, _

s( ) As Double)

Dim i, j As Integer

For i=0 To l-1

s( i )=0

For j=0 To k-1

s( i )+=p( i , j )*q( j )

Next j

Next i

End Sub

В языке Basic операционная система передает управление либо объекту программы, называемому формой, либо главной процедуре Sub Main.

7.3.2. Передача данных между процедурами


Схема обмена данными между процедурами:



Входные аргументы Выходные аргументы

Результат

Исходные данные Результаты



Входные параметры Выходные параметры

Возвращаемое значение функции разумно считать специфическим выходным параметром.

Механизмы передачи данных


Существуют 2 механизма передачи информации между процедурами: по значению и по адресу (ссылке, имени, наименованию).

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

Во втором механизме передается адрес аргумента и, следовательно, возможно его изменение в вызываемой процедуре. Этот механизм применяется для выходных данных (результатов работы) вызываемой процедуры.

В языке C по умолчанию реализован механизм передачи по значению, поэтому для обеспечения возврата результатов выполнения процедур применяются оператор & (найти адрес по имени) для аргумента, * (извлечь содержимое памяти по заданному адресу) для параметра и специальные типы переменных, называемые указателями (pointer).

Указатель – переменная, значением которой является адрес другой переменной. Указатель дает информацию о типе переменной, адрес которой он хранит.

Формат определения указателей:

<тип> *<имя>[, *<имя>]...;

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

Пример.

int *kol, *nom, x, y, *px;

double *rasst, *dlina;

..................................

px=&x;

y=*px;

Это эквивалентно y=x.

Более подробно работа с указателями будет рассмотрена позже.

В языке Basic можно реализовать оба механизма передачи аргументов. В версиях Visual Studio 2008, 2010 для этого используются ключевые слова ByVal (передача по значению) и ByRef (передача по адресу). По умолчанию в данных версиях, как и в языке С, реализована передача аргументов по значению (в отличие от предыдущих версий). То есть, если перед параметром опущено ключевое слово ByVal или ByRef, то среда подставит слово ByVal. Данных, подобных указателям, нет.

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


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

Формат прототипа:

<тип><имя>(<тип_параметра>[<имя>][, <тип_параметра>[<имя>]]...);

Пример. Прототип функции вычисления определенного интеграла методом трапеций.

double trap(double, double, int, double (*)(double));

Вызов функции:

.................................................................................

i=trap(0, alfa, 20, f1);// Без прототипа ошибка: 0 – целое значение

.................................................................................

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

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

Пример. Все математические функции имеют в качестве параметров и возвращаемого значения данные типа double. Их прототипы хранятся в заголовочном файле math.h. Например, double sin(double);

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

Передача скаляров

Возвращаемое значение

C Указание типа в заголовке функции обязательно. В подпрограммах для указания типа возвращаемого значения (которого нет) используется ключевое слово void. См. выше.

Пример.

float max(float a, float b){

........................

}

Basic

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

C

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

Пример.

Вызывающая процедура Вызываемая процедура

float a, max(float, float); float max(float a, float b){

int b; …………………………..

.................. }

y=2+3.5*max(a, b); // Целая переменная b преобразуется к типу float

Basic

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

Пример.

Function Max(ByVal a As Double, ByVal b As Double) As Double

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

Пример.

y=2+3.5*Max((a), (b))
Выходные данные

C

Поскольку в языках C и C++ реализована передача аргументов по значению, то для того, чтобы в вызываемой процедуре можно было изменять значение исходного аргумента, туда необходимо передавать адрес области памяти, где хранится аргумент. При этом функция не может изменить этот адрес, а содержание может. Для получения адреса используется оператор &. Следовательно, соответствующий параметр – указатель!!

Пример. Дана матрица {aij}, i,j=1...10. Найти max{aij} и его индексы.

float maxmatr(int m, int n, float a[ ][10], int *k, int *l){

float max;

int i, j;

max = a[0][0];

for(*k=*l=i=0; i

for(j=0; j

if(max

}

}

return max;

}// End maxmatr

Соответствующий фрагмент вызывающей процедуры имеет вид:

float maxmatr(int, int, float a[ ][10], int*, int* ), // Прототип

maxim, // Максимальный элемент

a[10][10]; // Исходная матрица

int m, n, // Ее размеры

str, col; // Индексы максимального элемента

..................................

maxim=maxmatr(m, n, a, &str, &col);

Употребление конструкции float a[ ][10] будет пояснено ниже.

Те же вычисления можно реализовать в виде подпрограммы.

void maxmatr( int m, int n,float a[][10], int *k, int *l, float *max){

.....................

Также везде надо заменить max на *max и убрать инструкцию return.

Пример.

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

Basic

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

Пример.

Sub maxmatr(ByVal m As Integer, ByVal n As Integer, ByRef a( , ) As Single, _

ByRef k As Integer, ByRef l As Integer, ByRef max As Single)

Передача массивов


C

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

При описании float a[10][10] обращение вида: <имя_функции>(a) эквивалентно обращению: <имя_функции>(&a[0][0]). Следовательно, массивы-параметры занимают память, отводимую в вызывающей процедуре массивам-аргументам, поэтому в прототипе и вызываемой процедуре допустимы их описания вида float b[ ], a[ ][10]; Длины всех измерений, кроме первого, надо задавать, чтобы правильно извлечь из памяти значения нужного элемента массива (см. формулу в параграфе "Распределение массивов"). Допустимо даже несоответствие размерностей аргумента и параметра.

Пример.

Аргументы Параметры

float a[5][5], b[36]; float a[ ], b[ ][6];

Пример. Вычислить: z=uТbu, где {ui]}, i=1...4; {bij]}, i,j=1...4.

/* Вычисление квадратичной формы */

int main( ){

float u[4], // Входной вектор

b[4][4], // Входная матрица

v[4], // Вектор b*u

scalar(float [ ], float [ ]);// Скалярное произведение векторов

int i, j;

/* Умножение матрицы на вектор */

void matrix(float [ ][4], float [ ], float [ ]);// Прототип
printf("Исходный вектор:\n");

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

scanf("%f", &u[ i ]);

}

printf("Исходная матрица:\n");

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

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

scanf("%f", &b[ i ][ j ]);

}

}

matrix(b, u, v);

printf("\n\n\nКвадратичная форма равна %.5g\n", scalar(v, u));

}// End main

/* Умножение матрицы на вектор */

void matrix(float a[ ][4], float x[ ], float y[ ]){

int i, j;

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

for(y[ i ]=j=0; j<4; j++){

y[ i ] += a[ i ][ j ]*x[ j ];

}

}

}// End matrix

/* Скалярное произведение векторов */

float scalar(float x[ ], float y[ ]){

int i;

float z;

for(z=i=0; i<4; i++){

z+=x[ i ]*y[ i ];

}

return z;

}// End scalar

Замечание. Приведенный пример решает поставленную задачу только для размера матрицы 4х4, что не позволяет использовать данное решение для матриц других размеров. Это резко снижает область его применения (свойство массовости алгоритма). Для повышения универсальности алгоритма при использовании массивов в языке C применяют стандартный прием: объявляют массив максимально ожидаемого размера (проигрывая в памяти), затем задают реальные его размеры, контролируя непревышение выделенной памяти. Ниже приводится решение для максимального размера матрицы 30х30.

Пример. Вычислить: z=uТbu, где {ui]}, i=1...m; {bij]}, i,j=1...m.

/* Вычисление квадратичной формы */

int main( ){

float u[30], // Входной вектор

b[30][30], //Входная матрица

v[30], // Вектор b*u

scalar(int, float [ ], float [ ]);// Скалярное произведение векторов

int i, j,

m; // Размер матрицы

bool fl; // true-неправильный ввод

/* Умножение матрицы на вектор */

void matrix(int, float [ ][30], float [ ], float [ ]);
fl=true;

while(fl){

clrscr( );

printf("Размер матрицы:"); scanf("%d",&m);

if(m<1||m>30){

printf("Размер матрицы должен быть в диапазоне [1:%d]\n",30);

getch();

}else fl=false;

}

printf("Исходный вектор:\n");

for(i=0; i

scanf("%f", &u[ i ]);

}

printf("Исходная матрица:\n");

for(i=0; i

for(j=0; j

scanf("%f", &b[ i ][ j ]);

}

}

matrix(m, b, u, v);

printf("\n\n\nКвадратичная форма равна %.5g\n", scalar(m, v, u));

getch( );

}// End main

/* Умножение матрицы на вектор */

void matrix(int m, float a[ ][30], float x[ ], float y[ ]){

int i, j;

for(i=0; i

for(y[ i ]=j=0; j

y[ i ] += a[ i ][ j ]*x[ j ];

}

}

}// End matrix

/* Скалярное произведение векторов */

float scalar(int m, float x[ ], float y[ ]){

int i;

float z;

for(z=i=0; i

z+=x[ i ]*y[ i ];

}

return z;

}// End scalar

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

Basic

В заголовке вызываемой процедуры для параметра-массива следует использовать ключевое слово ByRef. При этом копирования всего массива в локальную память процедуры не происходит и изменение его содержания приводит к изменению соответствующего аргумента в вызывающей процедуре.

Пример. Реализация предыдущей задачи. Только модуль Main.

Imports System.Console

Module Main

Вычисление квадратичной формы

Sub main( )

Dim i, j, m As Short ‘ Размер матрицы

Dim fl As Boolean ‘ true-неправильный ввод
fl=True

Do While fl

Clear( )

Write("Размер матрицы:") : m=ReadLine( )

If m<1

WriteLine("Размер матрицы должен быть > 0”)

ReadLine( )

Else

fl=False

End If

Loop

Объявление массивов

Dim u(m-1) As Single ‘ Входной вектор

Dim b(m-1,m-1) As Single ‘ Входная матрица

Dim v(m-1) As Single ‘ Вектор b*u

WriteLine("Исходный вектор (вводить столбиком)")

For i=0 To m-1

u( i )=ReadLine( )

Next

WriteLine("Исходная матрица (вводить столбиком)")

For i=0 To m-1

For j=0 To m-1

b( i, j)= ReadLine( )

Next j

Next i

matrix(m, b, u, v)

WriteLine( )

WriteLine("Квадратичная форма равна {0:g5}", scalar(m, v, u))

ReadLine( )

End Sub

Передача функций


В этом разделе рассмотрим передачу в качестве аргумента имени функции.

Этот специфический вид аргумента позволяет придать программе универсальность.

Пример. Найти y=min(f(x)), где {xi}, i=1...n. Существует множество методов нахождения экстремума функции многих переменных, практически не зависящих от вида функции, минимум которой отыскивается.

Сопряжение, т.е. имя функции со списком аргументов, min_fun(n, x, dx, eps, f) практически одинаково для различных методов. Здесь: n – число переменных; вектор (одномерный массив) x является одновременно и входным параметром (начальное приближение), и выходным (найденная точка минимума); dx – начальный шаг поиска; eps – точность нахождения минимума; f – имя (язык С) или адрес (язык Basic) минимизируемой функции .

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

Формат объявления: [<тип>](*<имя>)();

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

float f1(int, float [ ]), // Минимизируемая функция

min_fun(int, float [ ], float, float, float(*)( )) // Процедура минимизации

z;

..........................

z=min_fun(l, s, delta_x, epsilon, f1); // Обращение к процедуре

Вызываемая процедура:

float min_fun(int n, // Число измерений

float x[ ], // Начальное приближение

//Результат: точка минимума

float dx, // Начальный шаг поиска

float eps, // Заданная точность нахождения минимума

float(*f)( )){// Адрес минимизируемой функции

..........................

}

Следует различать записи:

<тип> *f( ); и <тип>(*f)( );

Первая – прототип функции без параметров, возвращающей указатель на <тип>, вторая – прототип указателя на функцию, возвращающей значение данного типа.

Пример. Вычислить и напечатать таблицу функции:

Интеграл вычислять методом трапеций.

/* Вычислить таблицу y=f(alfa) */

int main( ){

double f1(double), // Подинтегральная функция

y, // Значение интеграла

alfa, // Параметр

trap(double, double, int, double(*)( ));// Метод трапеций

for(alfa=2; alfa<3.05; alfa+=.1){

y=trap(.15, alfa, 20, f1);

printf("%10cальфа=%.1lf интеграл=%.6lf\n", ' ', alfa, y);

}

} // End main
/* Интегрирование методом трапеций */

double trap( double a, // Нижний предел интегрирования

double b, // Верхний --------------------

int k, // Число элементарных интервалов

double (*f)( )){// Подинтегральная функция

double dx, // Размер элементарного интервала

t;

int i;

dx=(b-a )/k;

t=((*f)(a)+(*f)(b))/2;

for(i=1; i

t += (*f)(a+i*dx);

}

return dx*t;

} // End trap
/* Подинтегральная функция */

double f1(double x){

return exp(x)*cos(pow(x, 3));

} // End f1

Basic

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

Формат объявления:

Delegate {Function | Sub} <имя> ([<список параметров>]) [As <тип>]

Для подпрограммы тип возвращаемого значения отсутствует.

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

Ниже приводится решение предыдущей задачи на языке Basic.

Module Main

Объявление делегата

DelegateFunction UndInteg(ByVal x AsDouble) AsDouble

Sub Main()

Dim y As Double ' Значение интеграла

Dim alfa As Double ' Параметр

Dim DelF1 As UndInteg ' Переменная типа делегата

DelF1 = AddressOf F1 ' Задание адреса функции-аргумента

For alfa = 2 To 3.05 Step 0.1

y = Integr(0.15, alfa, 20, DelF1)

Console.WriteLine("альфа={0:f1} интеграл={1:f4}", alfa, y)

Next

Console.ReadLine()

End Sub

End Module
Module Integ

' Интегрирование методом трапеций

' a-нижний предел, b-верхний предел, k-число элем.интервалов, F1-адрес

подинтегральной функции

Function Integr(ByVal a As Double, ByVal b As Double, _

ByVal k As Short, ByRef F As UndInteg)

Dim dx As Double 'Размер элементарного интервала

dx = (b - a) / k

Integr = (F(a) + F(b)) / 2

For i As Integer = 1 To k - 1

Integr += F(a + i * dx)

Next

Return Integr *dx

End Function

End Module
Imports System.Math

Module UnderInteg

' Подинтегральная функция

Function F1(ByVal x As Double) As Double

F1 = Exp(x) * Cos(Pow(x, 3))

End Function

End Module

Совет. Сравните реализации, найдите отличия, запомните и решите задачу ВЦ 24 с использованием делегата.


1   ...   16   17   18   19   20   21   22   23   ...   58


написать администратору сайта