Что обозначает запись int p1 x
Перейти к содержимому

Что обозначает запись int p1 x

  • автор:

С++ для тех кто не шарит

Около трех лет назад, когда я пошел в девятый класс, остро встал вопрос о том, что мне делать дальше. Было несколько вариантов, но в итоге я выбрал Киевский Колледж Связи, который, по словам знакомого, лучший в Киеве из области программирования. Ожидая потока знаний в мою голову, я стал сам потихоньку углубляться в IT и ко второму курсу, когда в учебной программе наконец начали появляться специализированные предметы, уже имел некоторый, относительно неплохой багаж знаний. Окрыленный надеждой получить если не все знания этого мира, то хотя бы половину, я с разбегу ударился в стену разумных доводов о том, что не стоит ожидать от этого места чего-то заоблачного. И вот я сижу на очередной паре и вставляю спички в глаза, ибо все это я успел выучить за первые две недели целенаправленного погружения в это «болото». К концу года, когда вот-вот должна начаться сессия и практика, я окончательно осознал, что на всю группу найдется максимум 3 человека, которые все поняли и могут спокойно оперировать полученными знаниями. (Двое из этих людей и так варились в этом программистском котле, а третий очень заинтересовался где-то к концу первой четверти и достиг очень неплохого уровня за минимальный промежуток времени). Тогда у меня и родилась идея написать максимально подробную «шпаргалку» для тех, кто заинтересован в успешной сдаче сессии но не понял что происходило весь прошлый год. К сожалению мало кто пользовался ею, по этому я решил предоставить ее более широкой общественности, ибо жалко добру пропадать.

Теперь по сути

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

Любая программа, будь то на телефоне или компьютере, строится на взаимодействии с оперативной памятью устройства. Вся RAM делится на ячейки по 1 байту каждая. Но для удобного использования все эти ячейки, когда до них доходит очередь, зачастую группируются для хранения большего объема данных. Так например целое число, если не вдаваться в подробности, храниться в блоке из четырех ячеек памяти, то есть занимает 4 байта памяти. В результате 32-битное целое число, а в 4 байтах именно 32 бита, может достигать значений от -2^31 до (2^31)-1. Степень 31, а не 32 потому что первый бит отвечает за знак числа, если 0 то +, если 1 то -.

Перейдем непосредственно к тому, как это выглядит на практике. Простейший пример:

int a; a = 1;

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

Строка int a; это непосредственно выделение тех четырех байт памяти под целое число. Мы взяли где-то в памяти 4 байта, присвоили им имя а и теперь можем обращаться к этой памяти как к чему-то единому, называя это целочисленной переменной.

Строка a = 1; задает нашей переменной значение равное 1 , фактически это значит, что в те 4 байта памяти запишется нечто вот такое: 00000000 00000000 00000000 00000001 . Тут 31 ноль и 1 единица в конце, это двоичное представление числа, что в нашем случае не сильно отличается от десятичного. Это действие, присвоение переменной начального значения, называется инициализацией.

Теперь немного упростим эту запись:

int a = 1;

Это абсолютно то же самое, только короче. Более того, многие среды разработки для С++ не дадут запустить код, если вы попытаетесь прочитать значение у переменной, которой не было задано значения. По этому присваивать переменным какое-то значение перед основными действиями являеться хорошим тоном.

Теперь попробуем немного похимичить:

int a = 1; int b = 2; int c = a + b;

Не сложно догадаться чему будет равно c . Это, на самом деле, и есть вся суть программирования, на основе одних данных получать другие. Все что тут произошло, можно переписать в виде 3 = 1 + 2 .

А сейчас покажу как довести учителя математики до истерики.

int c = 1; c = c + 1; c += 1; c++;

Строки со второй по четвертую на самом деле делают одно и тоже: просто к значению переменной c прибавляют 1 . Первый способ это прировнять c к этой же c только + 1 . Старая двойка, которая там была, перезапишется новым значением, в данном случае тройкой. То же самое, что мы делали в первом блоке, но теперь не просто число, а математическое выражение. Второй способ называется сложение с присваиванием. Это действие прибавляет к изменяемой переменной то число, которое написано в правой части, и потом записывает, получившийся результат, в эту же переменную — тройка перезапишется четверкой. Есть так же -= , *= , /= , думаю достаточно очевидно, что они сделают. Ну и третий способ: два плюса под ряд возле переменной — четверка перезапишется пятеркой.

++ / — меняет значение ТОЛЬКО на единицу ( — отнимает единицу).

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

Попробуем записать в перменную результат математического выражения 5 / 2 :

int a = 5 / 2;

На выходе мы ожидаем получить 2.5 , поскольку 5 / 2 = 2.5 Но компилятор уже спешит нас обламать и выводит в консоль ровно 2. Вернемся к битам и байтам. Наше число 5 (если обрезать левые 24 нолика) выглядит вот так: 00000101 а число 2 выглядит так: 00000010

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

В следующем примере попробуем получить дробное число. Для этого воспользуемся типом float при создании переменной а :

float a = 5. / 2;

После 5 стоит точка для того чтобы указать компилятору что нам требуеться дробный ответ. Если же оставить там 5 / 2 то ответ все равно будет целым числом.

Ура, дробное число получено, можешь отметить этот день в своем календаре)))

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

float a = 3; a = 3.5; a = 1.;

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

И так, мы уже знаем как создавать и работать с числовыми переменными. Но на этом много не напрограммируешь, необходимо как-то получать и обрабатывать данные из вне. Наподобие команды cout есть функция cin , которая принимает данные и записывает их в переменную.

int a; cout > a; cout bool a = true; a = 1 == 1; a = 1 == 2; a = a == false; a = !a;

Строка h = 1 == 1; . Что здесь происходит? Мы сравниваем 1 и 1 , равна ли единица единице. Разумеется что ответ правда, по этому на выходе мы получим значение true .

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

И так, переходим на третью строку: a = 1 == 2; 1 не равно 2 (как неожиданно), по этому результат сравнения будет false . Четвертая строка демонстрирует возможность сравнивать переменную с каким то значением или другой переменной. Поскольку на прошлой строке у нас получилась ложь, то сравнение false и false вернет, ожидаемо, правду. Ну и последняя строка, на которой мы берем значение обратное значению переменной a . Простыми словами это выглядит как не правда , что по другому является ложью .

Что ж, мы теперь полностью готовы к познанию того, как обрабатывать данные.

if(true)

Это, так называемое, условие if(условие) < действие >. Дословно это можно перевести так: «Если утверждение в круглых скобках правдиво, то выполнить действие в фигурных скобках». В нашем случае утверждение в скобках всегда правдиво, поскольку мы туда передаем заведомо правдивое значение.

Теперь немного усложним предыдущее условие.

bool a = false; if(a) < cout else

Вторая часть это else < действие >, которая дословно обозначает "Иначе, если условие не выполнилось, то. ". В данном случае, поскольку наша переменная a равна false , то выполниться блок else

Продолжаем познавать все прелести условий.

int a; cout > a; if(a < 5)< cout else if(i > 5) < cout 5" else

На этот раз у нас добавился блок else if(условие) < действие >, который будет выполнен в случае если первое условие не сработало. Дословно это значит "Иначе если первое условие не сработало, то проверить другое условие". Стоит заметить, что таких блоков с дополнительными условиями может быть сколько угодно, главное соблюдать порядок: if -> else if -> else .

Условия это конечно превосходно, но вся их сила раскрывается в тандеме с циклами. Всего есть три вида циклов, что на самом деле ложь, так как два из них в своей основе содержат третий. Сейчас станет понятнее.

Разберем фундаментальный вид циклов. Выглядит он так: while(условие) < действие >, и дословно значит: "Повторять действие в фигурных скобках, до тех пор, пока правдиво условие в круглых".

int a = 0; while(a != 5)< cout > a; >

Правда вот есть один нюанс, если мы поместим на место условия true , то цикл будет повторяться вечно. В нашем случае цикл будет повторяться до тех пор, пока не будет введено число 5.

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

int k = 0; while(k < 5)< cout for (int i = 0; i < 5; i++) < cout int i = 0; do < cout int i = 5; do < cout int A[10]; cout 

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

Нумерация в массивах, и вообще в программировании в целом, начинается с нуля, по этому первый элемент массива это 0, а последний - его длинна минус один.

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

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

Вот и настал тот момент, когда мы будем использовать циклы для каких-то осмысленных действий. В данном случае используем цикл for . Значения переменной i будут меняться от 0 до 9, что нам идеально подходит для использования в массиве.

for (int i = 0; i < 10; i++) < A[i] = i; >for (int i = 0; i

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

Теперь вернемся к адресам. Но для начала расскажу что такое ссылки и как их использовать. Ссылочная переменная, хранит в себе исключительно ссылку на первый байт переменной, на которую он ссылается (Каждая ячейка памяти имеет свой адрес, записанный в шестнадцатеричной системе. Для доступа к значению переменной по ссылке необходимо лишь знать ее тип данных и ссылку на первый байт). Объявляется путем прибавления к имени ссылочной переменной знака & .

int a = 2; int &ref = a; cout int a = 2; int *ptr = &a; cout int a = 2; cout int A[10]; A[0] = 3; A[1] = 1; cout int const а = 6; cout int const а = 6; float const pi = 3.14; int В[a]; B[0] = 2; cout

Создавать массив можно только с целым количеством ячеек, по этому передать туда pi не получиться, хоть это и константа. Заполним массив степенями двойки и выведем его на экран. Как видим, элементов у нас 6, как и было записано в константе m .

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

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

int a; cout > a; int *C = new int[a]; for (int i = 0; i < a; i++) < cout > C[i]; > for (int i = 0; i

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

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

int D[5][5]; cout int D[5][5]; for (int i = 0; i < 5; i++) < for (int j = 0; j < 5; j++) < D[i][j] = (i + 1)*(j + 1); >> for (int i = 0; i < 5; i++) < for (int j = 0; j < 5; j++) < cout cout

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

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

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

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

int a; cout > a; int **E = new int *[i]; for (int i = 0; i < a; i++)< E[i] = new int [a]; >for (int i = 0; i < a; i++) < for (int j = 0; j < a; j++) < cout cout

Мы создаем переменную E и указываем на двойную вложенность указателей.

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

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

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

К элементам динамических массивов так же можно обращаться с помощью ссылок и разыменовывания.

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

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

Страшно и сложно. Посмотрим на пример

int function(int a)

Теперь посмотрим как это использовать

int function(int a) < int b = a * a; return b; >int main()< int a = function(5); cout int a = 0; std::cout 

- модуль со строковым типом данных, которого в "чистом" С++ нету.

Небольшой экскурс. Строки, это на самом деле массивы символов, и чтоб не возникало возни с этим, создали модуль, который упрощает работу с ними. Если интересно как это устроенно на программном уровне, погугли "С-строки".

#include - Модуль, который позволяет взаимодействовать с WinAPI, проще говоря позволяет управлять системой виндовс. В нашем случае его можно использовать для этих двух строк.

SetConsoleCP(CP_UTF8); SetConsoleOutputCP(CP_UTF8);

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

Файл с кодом, приведенным в этой статье, ты можеш найти по этой ссылке на гитхаб

Заключение

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

Для дальнейшего изучения рекомендую сайт metanit. А так же можешь отдельно погуглить что-то из того что описано в этой статье, для более глубокого понимания темы.

  • начинающим
  • с++
  • как научиться программировать

Что обозначает запись int p1 x

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

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

Указателю можно присвоить адрес объекта того же типа, либо значение другого указателя. Для получения адреса объекта используется операция & :

int a ; int *pa ; // указатель pa хранит адрес переменной a

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

Разыменование указателя

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

#include int main() < int a ; int *pa ; // хранит адрес переменной a std::cout #include int main() < int a ; int b ; int *pa ; // указатель на переменную a int *pb ; // указатель на переменную b std::cout pa: address=0x56347ffc5c value=10 pb: address=0x56347ffc58 value=2 pa: address=0x56347ffc58 value=2 b value=125

Нулевые указатели

Нулевой указатель (null pointer) - это указатель, который не указывает ни на какой объект. Если мы не хотим, чтобы указатель указывал на какой-то конкретный адрес, то можно присвоить ему условное нулевое значение. Для определения нулевого указателя можно инициализировать указатель нулем или константой nullptr :

int *p1; int *p2<>;

Ссылки на указатели

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

#include int main() < int a ; int b ; int *p<>; // указатель int *&pRef

; // ссылка на указатель pRef = &a; // через ссылку указателю p присваивается адрес переменной a std::cout &:

int a ; int *pa ; std::cout >, >=, , ,==, !=. Операции сравнения применяются только к указателям одного типа. Для сравнения используются номера адресов:

#include int main() < int a ; int b ; int *pa ; int *pb ; if(pa > pb) std::cout

Консольный вывод в моем случае:

pa (0xa9da5ffdac) is greater than pb (0xa9da5ffda8)

Приведение типов

Иногда требуется присвоить указателю одного типа значение указателя другого типа. В этом случае следует выполнить операцию приведения типов с помощью операции (тип_указателя *) :

#include int main() < char c ; char *pc ; // указатель на символ int *pd <(int *)pc>; // указатель на int void *pv ; // указатель на void std::cout std::cout

Что обозначает запись: int *p1=&x; ?

Что обозначает запись: #define s1s2s3. sn q1q2. qm ?

В объявлении [static] tv namev [=value] запись tv определяет

В объявлении const [tc] namec=value; tc обозначает

Верна ли запись: template . ?

Что определяет запись: template . ?

Какая функция повторяет k раз символ ch в строке S , возвращает указатель на 1 ?

В функции fact(n) , которая вычисляет n! , значение аргумента n не может превосходить

Какая функция копирует содержимое CS2 в S1 , возвращает указатель на S1 ?

Диапазон данных типа int в Borland C++ под управлением MS-DOS составляет

C помощью какой функции определяется длина начального фрагмента CS1 , который не содержит ни одного символа из CS2 ?

Указатели

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

Определение указателя

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

тип_данных* название_указателя;

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

Например, определим указатель на объект типа int:

int* p;

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

#include int main()

Например, в моем случае консоль вывела "0x8" - некоторый адрес в шестнадцатеричном формате (обычно для представления адресов в памяти применяется шестнадцатеричная форма). Но также можно инициализировать указатель некоторым значением:

int* p<>;

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

int* p;

Хотя никто не запрещает не инициализировать указатели. Однако в общем случае рекомендуется все таки инициализировать, либо каким-то конкретным значением, либо нулем, как выше. Так, к примеру, нулевое значение в будущем позволит определить, что указатель не указывает ни на какой объект.

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

int* p1<>; int *p2<>;

Также стоит отметить, что размер значения указателя (хранимый адрес) не зависит от типа указателя. Он зависит от конкретной платформы. На 32-разрядных платформах размер адресов равен 4 байтам, а на 64-разрядных - 8 байтам. Например:

#include int main() < int *pint<>; double *pdouble<>; std::cout

В данном случае определены два указателя на разные типы - int и double. Переменные этих типов имеют разные размеры - 4 и 8 байт соответственно. Но размеры значений указателей будут одинаковы. В моем случае на 64-разрядной платформе размер обоих указателей равен 8 байтам.

Получение адреса и оператор &

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

int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number

Выражение &number возвращает адрес переменной number . Поэтому переменная pnumber будет хранить адрес переменной number . Что важно, переменная number имеет тип int, и указатель, который указывает на ее адрес, тоже имеет тип int. То есть должно быть соответствие по типу. Однако также можно использовать ключевое слово auto :

int number ; auto *pnumber ; // указатель pnumber хранит адрес переменной number

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

#include int main() < int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number std::cout

Консольный вывод программы в моем случае:

number addr: 0x1543bffc74

В каждом отдельном случае адрес может отличаться и при разных запусках программы может меняться. К примеру, в моем случае машинный адрес переменной number - 0x1543bffc74 . То есть в памяти компьютера есть адрес 0x1543bffc74, по которому располагается переменная number. Так как переменная x представляет тип int , то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x1543bffc74, 0x1543bffc75, 0x1543bffc76, 0x1543bffc77.

Указатели в C++

И указатель pnumber будет ссылаться на адрес, по которому располагается переменная number, то есть на адрес 0x1543bffc74.

Итак, указатель pnumber хранит адрес переменной number, а где хранится сам указатель pnumber? Чтобы узнать это, мы также можем применить к переменной pnumber операцию &:

#include int main() < int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number std::cout

Консольный вывод программы в моем случае:

number addr: 0xe1f99ff7cc pnumber addr: 0xe1f99ff7c0

Здесь мы видим, что переменная number располагается по адресу 0xe1f99ff7cc , а указатель, который хранит этот адрес, - по адресу 0xe1f99ff7c0 . Из вывода видно, что обе переменные хранятся совсем рядом в памяти

Получение значения по адресу

Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной number. Для этого применяется операция * или операция разыменования ("indirection operator" / "dereference operator"). Результатом этой операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной number:

#include int main() < int number ; int *pnumber ; std::cout Address = 0x44305ffd4c Value = 25

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

int n1 ; int *pn1 ; // указатель pn1 хранит адрес переменной n1 int n2 < *pn1>; // n2 получает значение, которое хранится по адресу в pn1 std::cout int x = 10; int *px = &x; *px = 45; std::cout

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *