Помощь - Поиск - Пользователи - Календарь
Полная версия этой страницы: Что означает этот код?
Форум разработчиков электроники ELECTRONIX.ru > Сайт и форум > В помощь начинающему > Программирование
RW6MKA
Здравствуйте уважаемые форумчане. Возник вопрос при оптимизации кода. Было
Код

                data[0] = w1_receive_byte();//читаем два байта с температурой
        data[1] = w1_receive_byte();
            //загоняем в двух байтную переменную
        temp = data[1];
        temp = temp<<8;
        temp |= data[0];

Подсказали что лучше использовать такое выражение
Код
        *((char *)&Temp;) = w1_receive_byte();
        *((char *)&Temp; + 1) = w1_receive_byte();

Вот никак не могу понять смысла этого кода. Я так понимаю что * и & это операции над указателями , а () приведение к типу но....
Вообщем если не трудно объясните начинающему подробно смысл сей конструкции.
_Артём_
Цитата(RW6MKA @ Mar 8 2014, 19:05) *
Код
        *((char *)&Temp;) = w1_receive_byte();
        *((char *)&Temp; + 1) = w1_receive_byte();

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

У вас этот код компилируется? Врядли...Точки с запятой лишние.

Код
unsigned short Temp;
        *((char *)&Temp) = w1_receive_byte();
        *((char *)&Temp + 1) = w1_receive_byte();

Смысл такой - берётся адрес Temp и в него пишется 1 байт.Во второй строчке - пишется ещё байт по адресу следующего байта.
RW6MKA
А более подробно можно. Конкретно по каждому знаку. Просто общий смысл как бы я сразу понял ( из того что предложили сделать замену, значит этот код выполняет ту же функцию). Хотелось бы полностью понимать, что бы в последствии не было проблем с использованием подобных конструкций.
aaarrr
Цитата(RW6MKA @ Mar 8 2014, 19:05) *
Подсказали что лучше использовать такое выражение

А чем лучше - не подсказали? А на деле оно хуже.

Цитата(RW6MKA @ Mar 8 2014, 19:29) *
А более подробно можно. Конкретно по каждому знаку.

&Temp - взяли адрес Temp
(char *)&Temp - привели к типу указателя на char
*((char *)&Temp) = w1_receive_byte() - по указателю на char, который равен адресу Temp, занесли результат w1_receive_byte().
_Артём_
Цитата(RW6MKA @ Mar 8 2014, 19:29) *
А более подробно можно

Берётся адрес Temp: &Temp
приводится к указателю на char: (char *)(полученный адрес)
по указателю на char делается запись того что вернёт функция:
*(указатель на char)=w1_();

Со второй строчкой также - только ещё инкремент указателя есть.
RW6MKA
Ну вроде экономия памяти при компиляции. Можно конечно оставить и свой код, места мне хватает, но задело, не сталкивался с подобным и поэтому хочу понять. Народ, не сочтите за труд, поэтапно объясните. &Temp - выдаст адрес переменной. (char*)&Temp - приведение типа адреса переменной Temp к типу char(не пойму зачем знак *) и наконец зачем знак * в самом начале выражения?

Цитата(aaarrr @ Mar 8 2014, 19:36) *
А чем лучше - не подсказали? А на деле оно хуже.


&Temp - взяли адрес Temp
(char *)&Temp - привели к типу указателя на char
*((char *)&Temp) = w1_receive_byte() - по указателю на char, который равен адресу Temp, занесли результат w1_receive_byte().

Ага, понял. А в следующем выражении заносят по адресу Temp+1. то есть получаем эти два байта записанные в соседних адресах или я что то путаю?
aaarrr
* - указатель

Без лишней памяти лучше будет написать так:
Код
temp = (char)w1_receive_byte();
temp = (temp << 8) | (char)w1_receive_byte();

(char) нужны только на случай, если w1_receive_byte() возвращает что-то другое.

Вариант
Код
*((char *)&Temp) = w1_receive_byte();
*((char *)&Temp + 1) = w1_receive_byte();

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

Цитата(RW6MKA @ Mar 8 2014, 19:54) *
Ага, понял. А в следующем выражении заносят по адресу Temp+1. то есть получаем эти два байта записанные в соседних адресах или я что то путаю?

Да, так.
RW6MKA
Цитата(_Артём_ @ Mar 8 2014, 19:37) *
Берётся адрес Temp: &Temp
приводится к указателю на char: (char *)(полученный адрес)
по указателю на char делается запись того что вернёт функция:
*(указатель на char)=w1_();

Со второй строчкой также - только ещё инкремент указателя есть.

Вот этого не могу понять. Что это - приводиться к указателю? Приводится к типу понятно, а это выражение не пойму.
Harvester
Ужас какой-то. Это как раз тот случай, когда достоинства языка превращают в недостатки кода sm.gif
Вполне достаточно такого кода, все понятно и ничего лишнего:
Код
temp = w1_receive_byte();
temp <<= 8;
temp |= w1_receive_byte();

Хотя, если data[] - локальная переменная и больше нигде не используется, компилятор ее сам выкинет.
aaarrr
Цитата(RW6MKA @ Mar 8 2014, 19:58) *
Вот этого не могу понять. Что это - приводиться к указателю? Приводится к типу понятно, а это выражение не пойму.

Есть тип char, есть тип "указатель на char" - вот к последнему и приводится.
RW6MKA
Цитата(aaarrr @ Mar 8 2014, 20:02) *
Есть тип char, есть тип "указатель на char" - вот к последнему и приводится.

Да, здесь в моих знаниях пробел.(( Спасибо всем за столь подробные объяснения. Чувствую, мне еще учить и учить))))
Еще раз всем спасибо.
Harvester
Цитата(RW6MKA @ Mar 8 2014, 19:58) *
Вот этого не могу понять. Что это - приводиться к указателю? Приводится к типу понятно, а это выражение не пойму.

Указатель - это просто переменная, хранящая адрес в памяти.
Temp - 2-байтная переменная. Если указатель на Temp равен A, то содержимое переменной лежит по адресам [A] и [A+1]. Если мы просто инкрементируем указатель, он станет равным A+2, т.е. будет указывать на следующую 2-байтную переменную. А поскольку нам надо писать "внутрь" переменной, мы приводим тип к указателю на char. Тем самым говорим компилятору, что при инкременте указателя, его значение нужно увеличить не на 2, а на 1. Это позволит нам выполнить запись по адресу [A+1].
SSerge
Цитата(Harvester @ Mar 8 2014, 23:01) *
Ужас какой-то. Это как раз тот случай, когда достоинства языка превращают в недостатки кода sm.gif
Вполне достаточно такого кода, все понятно и ничего лишнего:
Код
temp = w1_receive_byte();
temp <<= 8;
temp |= w1_receive_byte();

Немного позанудствую.
С первой и второй строкой всё нормально, а вот с третьей...
Результат выполнения этого может быть разным в зависимости от типов данных переменной temp и типа, возвращаемого функцией w1_receive_byte(). Даже если он определён как char, это ещё ничего не гарантирует, в С char может быть как знаковым так и беззнаковым, отдано на откуп реализации.
Для конкретного компилятора может и прокатит, как правило сейчас char считается беззнаковым.
Но если заботиться о переносимости, то надёжнее явно привести к беззнаковому типу:

temp |= (unsigned char)w1_receive_byte();

или, ещё лучше и короче c использованием типов из stdint.h :
temp |= (uint8_t)w1_receive_byte();
RW6MKA
Цитата(Harvester @ Mar 8 2014, 20:25) *
Указатель - это просто переменная, хранящая адрес в памяти.
Temp - 2-байтная переменная. Если указатель на Temp равен A, то содержимое переменной лежит по адресам [A] и [A+1]. Если мы просто инкрементируем указатель, он станет равным A+2, т.е. будет указывать на следующую 2-байтную переменную. А поскольку нам надо писать "внутрь" переменной, мы приводим тип к указателю на char. Тем самым говорим компилятору, что при инкременте указателя, его значение нужно увеличить не на 2, а на 1. Это позволит нам выполнить запись по адресу [A+1].

То есть если бы мы сделали вот так
Код
((char)&Temp) = w1_receive_byte();//здесь просто адрес переменной привели к типу char и производим по нему запись т.к. переменная двух байтовая то адрес занимает грубо две ячейки и мы одну из них заняли.
((char)&Temp + 1) = w1_receive_byte();//в этой строке следующий адрес переменной приводим к типу char производим по нему запись, но запись получается уже по другим двум ячейкам.

в результате получаем запись двух байтов по двум разным адресам переменной.
А если приводить не к типу, а указателю на тип
Код
(char*)&Temp
и производить запись в указатель всего этого безобразия
Код
*((char*)&Temp)
, то мы записываем данные по, скажем, первой ячейке адреса, предназначенной для младшего байта, а при
Код
*((char*)&Temp+1)
по второй ячейке этого же адреса, предназначенной для старшего байта.
Я на правильном пути?
И ещё попутный вопрос. Правильно ли я выбрал тип переменной char если переменная это дробные числа имеющие разный знак? Может лучше использовать тип float?
вот функция целиком
Код
char temp_18b20(){//функция преобразует полученые с датчика 18b20 данные в температуру

    char temp = 0;
    if(TD_find()==1)//если устройство присутствует на шине
    {
        TD_sendcmd(0xcc);//пропустить ROM код, так ка датчик в устройстве один и не требуется идентификация
        TD_sendcmd(0x44);//команда датчику преобразовать температуру
        _delay_ms(750);//преобразование в 12 битном режиме занимает 750ms
        TD_find();//снова опрос присутствия и пропуск кода
        TD_sendcmd(0xcc);
        TD_sendcmd(0xbe);//команда датчику передать байты (у 18b20 в первых двух байтах содержится температура)
        //читаем два байта с температурой и записываем оба байта в двух байтовую переменную
        temp = (char)TD_receive_byte();
        temp = (temp << 8) | (char)TD_receive_byte();
        //переводим в градусы
        //пока кода нет, но в результате будут температурные данные
    }
    //возвращаем температуру
    return temp;
}
aaarrr
Цитата(RW6MKA @ Mar 9 2014, 07:47) *
То есть если бы мы сделали вот так
Код
((char)&Temp) = w1_receive_byte();//здесь просто адрес переменной привели к типу char и производим по нему запись т.к. переменная двух байтовая то адрес занимает грубо две ячейки и мы одну из них заняли.
((char)&Temp + 1) = w1_receive_byte();//в этой строке следующий адрес переменной приводим к типу char производим по нему запись, но запись получается уже по другим двум ячейкам.

в результате получаем запись двух байтов по двум разным адресам переменной.

Некоторая каша получилась.
1. По приведенному к типу char адресу переменной записать ничего нельзя, это и не скомпилируется.
Это примерно как написать: 5 = 10;
2. Размерность адреса никак не связана с размерностью переменной.
Нужно понимать, что адрес, полученный через "&", является просто числом, а не новой переменной.

Цитата(RW6MKA @ Mar 9 2014, 07:47) *
А если приводить не к типу, а указателю на тип
Код
(char*)&Temp
и производить запись в указатель всего этого безобразия
Код
*((char*)&Temp)
, то мы записываем данные по, скажем, первой ячейке адреса, предназначенной для младшего байта, а при
Код
*((char*)&Temp+1)
по второй ячейке этого же адреса, предназначенной для старшего байта.
Я на правильном пути?

Ага, на правильном.

Цитата(RW6MKA @ Mar 9 2014, 07:47) *
И ещё попутный вопрос. Правильно ли я выбрал тип переменной char если переменная это дробные числа имеющие разный знак? Может лучше использовать тип float?
вот функция целиком
Код
char temp_18b20(){//функция преобразует полученые с датчика 18b20 данные в температуру
char temp = 0;
if(TD_find()==1)//если устройство присутствует на шине
{
    ...
    temp = (char)TD_receive_byte();
    temp = (temp << 8) | (char)TD_receive_byte();
    //переводим в градусы
    //пока кода нет, но в результате будут температурные данные
    ...
    //возвращаем температуру
    return temp;
}

Переменная temp должна иметь тип как минимум short, чтобы в нее поместилось два char'а.
Float имеет смысл использовать на более высоком уровне, например:
Код
short temp_18b20()
{
    ...
}

int main(void)
{
    float t;

    t = (float)temp_18b20() / 16;
    pritnf("Temp = %1.2f\n", t);
}
RW6MKA
Цитата(aaarrr @ Mar 9 2014, 08:09) *
По приведенному к типу char адресу переменной записать ничего нельзя, это и не скомпилируется.
Это примерно как написать: 5 = 10;

Понял. Да, мог бы конечно и сам понять что в адрес ничего не запишешь, можно записать по указателю адреса.
Цитата
Переменная temp должна иметь тип как минимум short, чтобы в нее поместилось два char'а.

Но short вроде бы тип для целых чисел, а у меня переменная Temp потом будет преобразовывать свое содержимое из двух байт двоичного кода в десятичные показания температуры с точностью до десятых. И сама функция будет возвращать такое число. Поэтому я и спросил за float, может лучше это тип использовать?
aaarrr
Обычно из таких - низкоуровневых - функций данные в плавающей точке все же не вытаскивают. Но если очень хочется, то можно.
Другой вопрос, нужен ли будет на деле этот float потом.
RW6MKA
Цитата(aaarrr @ Mar 9 2014, 11:30) *
Обычно из таких - низкоуровневых - функций данные в плавающей точке все же не вытаскивают. Но если очень хочется, то можно.
Другой вопрос, нужен ли будет на деле этот float потом.

Хорошо, может это стрельба по воробьям из пушки. Какой тип вы для этой функции и переменной порекомендуете, если в результате функция должна вернуть число от -45,5 до 45,5 ?
aaarrr
Цитата(RW6MKA @ Mar 9 2014, 12:20) *
Какой тип вы для этой функции и переменной порекомендуете, если в результате функция должна вернуть число от -45,5 до 45,5 ?

Я бы оставил short, а привести/масштабировать всегда можно по месту.
SM
Цитата(RW6MKA @ Mar 9 2014, 12:20) *
от -45,5 до 45,5 ?


Умножте это дело на 10, получите -455...455, это влезает в 16 битное целое с запасом.
RW6MKA
Во общем из советов я прихожу к выводу, что float лучше не использовать, а все постараться свести к типу целых чисел например int или short. Кстати вот еще вопрос, чем они отличаются? Как я понял и тот и тот тип могут быть 16 битным.
aaarrr
Могут. На 8-16 битных архитектурах ничем не отличаются. На архитектурах большей разрядности short может (по стандарту никто не обязывает) остаться 16 битным, а int соответствует разрядности процессора.
RW6MKA
Понятно. Тогда с учетом того, что я пишу код в AVRStudio под tiny2313 будет ли правильной вот эта функция
Код
uint16_t temp_18b20(){//функция получает с датчика данные с температурой в виде двух байт

    uint16_t temp = 0;
    if(TD_find()==1)//если устройство присутствует на шине
    {
        TD_sendcmd(0xcc);//пропустить ROM код, так ка датчик в устройстве один и не требуется идентификация
        TD_sendcmd(0x44);//команда датчику преобразовать температуру
        _delay_ms(750);//преобразование в 12 битном режиме занимает 750ms
        TD_find();//снова посылаем Presence и Reset
        TD_sendcmd(0xcc);
        TD_sendcmd(0xbe);//команда датчику передать байты (у 18b20 в первых двух байтах содержится температура)
        //читаем два байта с температурой и записываем оба байта в двух байтную переменную
        temp = TD_receive_byte();
        temp = (temp << 8) | TD_receive_byte();
        
    }
    //возвращаем температуру
    return temp;
}
aaarrr
Правильнее будет int16_t, а не uint - результат ведь знаковый. И проверьте, что TD_receive_byte() возвращает char, а не больше.
SM
Цитата(aaarrr @ Mar 9 2014, 13:38) *
Правильнее будет int16_t


Не совсем правильнее. Еще один момент есть - при таком раскладе, если Если TD_receive_byte() возвращаяет signed char, то операция temp = (temp << 8) | TD_receive_byte() убьет старший байт, если 7-ой бит там окажется в единице, и расширится до 16 бит. Так что, поосторожнее, надо бы на всяк случай написать temp = (temp << 8) | ((unsigned char)TD_receive_byte()). Вариант с прямой записью байтов на свои места по указателю в данном случае не имеет под собой таких "засад".
RW6MKA
Цитата(aaarrr @ Mar 9 2014, 13:38) *
Правильнее будет int16_t, а не uint - результат ведь знаковый. И проверьте, что TD_receive_byte() возвращает char, а не больше.

Нет, знак еще находится в коде, т.е. если 11-15 биты 1,то знак будет -. Так же и с дробной частью. Она заложена в 0-3 битах. Все это буду извлекать в отдельной функции с преобразованием в код LCD дисплея. Функция TD_receive_byte() возвращает uint8_t.
aaarrr
Цитата(RW6MKA @ Mar 9 2014, 13:48) *
Нет, знак еще находится в коде, т.е. если 11-15 биты 1,то знак будет -.

Это понятно. Но если Вы хотите, чтобы этот знак был учтен в дальнейшем, то результат должен быть знаковый:
short s = 0xffff;
unsigned short us = 0xffff;
float f;

f = s; // f = -1;
f = us; // f = 65535

Цитата(RW6MKA @ Mar 9 2014, 13:48) *
Функция TD_receive_byte() возвращает uint8_t.

Тогда порядок.
RW6MKA
Цитата(aaarrr @ Mar 9 2014, 14:06) *
Но если Вы хотите, чтобы этот знак был учтен в дальнейшем, то результат должен быть знаковый:

Понятно. Учту.
Спасибо всем за подробные ответы. Надеюсь и в дальнейшем на вашу помощь.
Xenia
Судя по стартовому посту темы, вопрос касался оптимизации кода, т.е. стремления избавиться от операции 8-кратного сдвига, который МК обычно не умеют делать одной инструкцией. А то и вызывают на этом месте библиотечную функцию, которая осуществляет сдвиги в цикле со счетчиком. Тогда как ответы в общем-то сводятся к тому, как этот сдвиг красивше записать. Т.е. в плане оптимизации эти ответы не только бесполезны, но и уже негативно проявили себя по части всевозможных ошибок при реализации.

В этом плане выражение
Код
*(char *)&Temp = TD_receive_byte();
*((char *)&Temp + 1) = TD_receive_byte();
было вполне оптимальным, с тем лишь недостатком, что оказалось непонятным топикстартеру. Т.е. его и надо было просто разъяснить, но не возвращаться к сдвигу.

На этот счет могу предложить альтернативу с union, которая в максимальной степени оптимальна, но гораздо проще в понимании:
Код
uint16_t temp_18b20()
{
  union {
    unsigned char byte[2];
    uint16_t word;
  } temp;

  ...
  temp.byte[1] = TD_receive_byte();
  temp.byte[0] = TD_receive_byte();
   ...
  return temp.word;
}

Здесь union располагает в одной и той же памяти 2 байта byte и одно слово word, тем самым, позволяя заполнить последнее по частям.
SM
Цитата(Xenia @ Mar 9 2014, 14:34) *
т.е. стремления избавиться от операции 8-кратного сдвига, который МК обычно не умеют делать одной инструкцией.

Вот это, как раз, большинство 8-разрядных МК делают одной командой с легкостью, так как 16-битные регистры образуются из пар 8-битных, и такой сдвиг оптимизируется в пересылку сразу в нужный регистр.
aaarrr
Цитата(Xenia @ Mar 9 2014, 14:34) *
Судя по стартовому посту темы, вопрос касался оптимизации кода, т.е. стремления избавиться от операции 8-кратного сдвига, который МК обычно не умеют делать одной инструкцией. А то и вызывают на этом месте библиотечную функцию, которая осуществялет сдвиги в цикле со счетчиком.

На 8? В 21-м веке? Со счетчиком? Не верю.


Цитата(Xenia @ Mar 9 2014, 14:34) *
Здесь union располагает в одной и той же памяти 2 байта byte и одно слово word, тем самым, позвояяя заполнить последнее по частям.

Во-первых, громоздко; во-вторых, что будем с эндианизьмом делать?
Xenia
Цитата(SM @ Mar 9 2014, 14:41) *
Вот это, как раз, большинство 8-разрядных МК делают одной командой с легкостью, так как 16-битные регистры образуются из пар 8-битных, и такой сдвиг оптимизируется в пересылку сразу в нужный регистр.


Всё это лишь надежда на то, что компилятор при оптимизации исправит наш дурацкий код. sm.gif Но тогда зачем такой код писать? Тогда как через union оно так красиво записывается, что глаз не оторвать! sm.gif
_Артём_
Цитата(aaarrr @ Mar 9 2014, 14:46) *
Во-первых, громоздко; во-вторых, что будем с эндианизьмом делать?

А что, большие индейцы разве не вымерли?
В 21 веке только мелкие как правило попадаются...Или нет?
aaarrr
Цитата(_Артём_ @ Mar 9 2014, 14:50) *
В 21 веке только мелкие как правило попадаются...Или нет?

У Вас есть роутер?
SM
Цитата(Xenia @ Mar 9 2014, 14:49) *
Но тогда зачем такой код писать?

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

PS
А union мне не нравится, не люблю лишние сущности. Напрямую оно понятнее, так как сразу все видно и понятно, без заглядывания в описание юниона. Главное язык программирования знать, чтобы не смотреть на эту запись, как ТС в начале темы.

PPS
А с эндианизьмом - #ifdef ....
_Артём_
Цитата(aaarrr @ Mar 9 2014, 14:58) *
У Вас есть роутер?

Нет, у меня нет роутера. Что это такое? sm.gif
Вы намекаете, что MIPS имеет big endian? Не знал...
SM
Цитата(_Артём_ @ Mar 9 2014, 15:08) *
что MIPS имеет big endian? Не знал...


А еще многие имеют программируемый endian. Например TI C6000, да и АРМы тоже не все little, особенно из кортексов-А
adnega
Господа программисты, особенно начинающие, обратите серьезное внимание на решение от Ксении.
Хватит уже думать на уровне инструкций, переходите на уровень управления данными!
union и struct позволяют сделать код в разы "читабельней" и "управляемей".
Завтра вместо DS18B20 возьмете DS1820 и будете по всему коду искать сдвиги и прогие логические операции?!

Код
uint16_t temp_18b20()
{
  union {
    unsigned char byte[2];
        uint16_t  word;
      struct
       {
      uint16_t  ds18b20_f:4;
          uint16_t  ds18b20_i:8;
        };
        struct
        {
           uint16_t  ds1820_f:1;
           uint16_t  ds1820_i:8;
        };
        struct
        {
           uint16_t  ds_uni_f:N // ваше число бит для дробной части
           uint16_t  ds_uni_i:8; // целая часть температуры
        }
  } temp;

  ...
  temp.byte[1] = TD_receive_byte();
  temp.byte[0] = TD_receive_byte();
   ...
  return temp.ds_uni_i;
}


Меняете в одном месте N и все!
SM
Цитата(adnega @ Mar 9 2014, 15:21) *
union и struct позволяют сделать код в разы "читабельней" и "управляемей".


Сильно поспорю. Читаемее то, для прочтения чего не надо искать хидер, где описан юнион, читать и разбирать этот хидер, и возвращаться к исходнику с использованием юнита (а без этого подумается, что тут просто запись в структуру, не юнион). Прямое средство запутывания следов. А прямая запись по указателю, как раз, сразу читается, без поиска концов по хидерам.
aaarrr
Цитата(SM @ Mar 9 2014, 15:26) *
Прямое средство запутывания следов.

Соглашусь полностью. Запутывание и загромождение.
RW6MKA
Уважаемые форумчане, перестаньте в присутствии новичка ругаться непонятными терминами и спорить biggrin.gif . Вопроса про оптимизацию кода я не ставил, как я уже писал, места мне вполне хватает. Я просил объяснить кусок кода и мне таки его объяснили. Возник вопрос о правильности применения того или иного типа переменной и функции и мне опять таки вежливо и подробно все объяснили. За что всем огромное спасибо. Что касается
Цитата
Хватит уже думать на уровне инструкций, переходите на уровень управления данными!
Вы извините, я не программист и не собираюсь им становиться, это просто увлечение на уровне не особо сложных полезных поделок с простенькими МК. Так что не спорьте между собой что лучше и как лучше, просто по возможности объясняйте таким новичкам как я что нам не понятно и мы будем вам очень благодарны a14.gif
aaarrr
Не обращайте внимания, день сегодня такой... выходной. Вот народ и спорит от скуки sm.gif
Для просмотра полной версии этой страницы, пожалуйста, пройдите по ссылке.
Invision Power Board © 2001-2025 Invision Power Services, Inc.