Студопедия

КАТЕГОРИИ:


Архитектура-(3434)Астрономия-(809)Биология-(7483)Биотехнологии-(1457)Военное дело-(14632)Высокие технологии-(1363)География-(913)Геология-(1438)Государство-(451)Демография-(1065)Дом-(47672)Журналистика и СМИ-(912)Изобретательство-(14524)Иностранные языки-(4268)Информатика-(17799)Искусство-(1338)История-(13644)Компьютеры-(11121)Косметика-(55)Кулинария-(373)Культура-(8427)Лингвистика-(374)Литература-(1642)Маркетинг-(23702)Математика-(16968)Машиностроение-(1700)Медицина-(12668)Менеджмент-(24684)Механика-(15423)Науковедение-(506)Образование-(11852)Охрана труда-(3308)Педагогика-(5571)Полиграфия-(1312)Политика-(7869)Право-(5454)Приборостроение-(1369)Программирование-(2801)Производство-(97182)Промышленность-(8706)Психология-(18388)Религия-(3217)Связь-(10668)Сельское хозяйство-(299)Социология-(6455)Спорт-(42831)Строительство-(4793)Торговля-(5050)Транспорт-(2929)Туризм-(1568)Физика-(3942)Философия-(17015)Финансы-(26596)Химия-(22929)Экология-(12095)Экономика-(9961)Электроника-(8441)Электротехника-(4623)Энергетика-(12629)Юриспруденция-(1492)Ядерная техника-(1748)

Auto, extern, register, static, mutable 4 страница




int sqrMas[10][2] = {

{1, 1},

{2, 4},

{3, 9),

{4, 16),

(5, 25},

(6, 36},

{7, 49},

(8, 64},

(9, 81},

{10, 100}

};

 

int main ()

{

int i, j;

for (;;) {

cout << "Введіть число від 1 до 10: ";

cin >> i;

if (i>=1)&&(i<=10) break;

}

 

// Пошук значення i.

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

if (sqrMas[j][0] == i) break;

cout << "Квадрат числа " << i << " дорівнює " << sqrMas[j][1];

 

getch (); return 0;

}

Глобальні масиви ініціалізувалися на початку виконання програми, а локальні – під час кожного виклику функції, у якій вони містяться. Розглянемо такий приклад:

Код програми 5.16. Демонстрація ініціалізації елементів двовимірного масиву

#include <iostream> // Для потокового введення-виведення

#include <cstring> // Для роботи з рядковими типами даних

using namespace std; // Використання стандартного простору імен

 

void fun_1();

 

int main ()

{

fun_1();

fun_1();

 

getch (); return 0;

}

 

void fun_1() {

char sMas[80] ="Це просто тест\n";

cout << sMas;

strcpy (sMas, "Змінено\n"); // Змінюємо значення рядка sMas.

cout << sMas;

}

У процесі виконання ця програма відображає на екрані такі результати:

Це просто тест

Змінено

Це просто тест

Змінено

У цій програмі масив sMas ініціалізувався під час кожного виклику функції fun_1(). Той факт, що під час її виконання масив sMas змінюється, ніяк не впливає на його повторну ініціалізацію при подальших викликах функції fun_1(). Тому під час кожного входження в неї на екрані з'являється такий текст:

Це просто тест

5.5.2. "Безрозмірна" ініціалізація масивів

Припустимо, що ми використовуємо такий варіант ініціалізації масивів для побудови таблиці повідомлень про помилки:

char e1[14] = "Ділення на 0\n";

char е2[23] = "Кінець файлу\n";

char е3|[21] = "У доступі відмовлено\n";

Неважко припустити, що вручну незручно підраховувати символи у кожному повідомленні, щоб визначити коректний розмір масиву. На щастя, у мові програмування C++ передбачено можливість автоматичного визначення довжини масивів шляхом використання їх "безрозмірного" формату. Якщо в настанові ініціалізації масиву не вказано його розмір, то мова програмування C++ автоматично створить масив, розмір якого буде достатнім для зберігання всіх значень ініціалізацій. При такому підході попередній варіант ініціалізації масивів для побудови таблиці повідомлень про помилки можна переписати так:

char e1[] = "Ділення на 0\n";

char e2[] = "Кінець файлу\n";

char е3[] = "У доступі відмовлено \n|";

Крім зручності в первинному визначенні масивів, метод "безрозмірної" ініціалізації дає змогу змінити будь-яке повідомлення без переліку його довжини. Тим самим усувається можливість внесення помилок, викликаних випадковим прорахунком.

"Безрозмірна" ініціалізація масивів не обмежується одновимірними масивами. Під час ініціалізації багатовимірних масивів Вам треба вказати усі дані, за винятком крайньої зліва розмірності, щоб С++-компілятор міг належним чином індексувати масив. Використовуючи "безрозмірну" ініціалізацію масивів, можна створювати таблиці різної довжини, даючи змогу компіляторові автоматично виділяти область пам'яті, достатню для їх зберігання.

У такому прикладі масив sqrMas оголошується як "безрозмірний":

int sqrMas[][ 2 ]= {

1, 1,

2, 4,

3, 9,

4, 16,

5, 25,

6, 36,

7, 49,

8, 64,

9, 81,

10, 100

};

Перевага такої форми оголошення перед "габаритною" (з точним указанням усіх розмірностей) полягає у тому, що програміст може подовжувати або укорочувати таблицю значень ініціалізації, не змінюючи розмірності масиву.

5.6. Масиви рядків

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

5.6.1. Побудова масивів рядків

Для побудови масиву рядків використовується двовимірний символьний масив, у якому розмір лівого індексу визначає кількість рядків, а розмір правого – максимальну довжину кожного рядка. Наприклад, у процесі виконання такої настанови оголошується масив, призначений для зберігання 30 рядків завдовжки 80 символів:

char strMas2[30][80];

Масив рядків – це спеціальна форма двовимірного масиву символів.

Отримати доступ до окремого рядка достатньо просто: достатньо вказати тільки лівий індекс. Наприклад, така настанова викликає функцію gets () для запису третього рядка масиву:

gets (strMas2[2]);

Щоб краще зрозуміти, як потрібно поводитися з масивами рядків, розглянемо таку коротку програму, яка приймає рядки тексту, що вводяться з клавіатури, і відображає їх на екрані після введення порожнього рядка.

Код програми 5.17. Демонстрація введення рядка тексту і відображення його на екрані

#include <iostream> // Для потокового введення-виведення

#include <cstdio> // Для підтримки системи введення-виведення

using namespace std; // Використання стандартного простору імен

 

int main ()

{

int t, i;

char text[100][80];

for (t=0; t<100; t++) {

cout << t << ": ";

gets (text[t]);

if (!text[t][0]) break; // Вихід з циклу за

} // порожнім рядком.

 

// Відображення рядків на екрані.

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

cout << text[i] << "\n";

 

getch (); return 0;

}

Звернемо Вашу увагу на те, як у програмі виконується перевірка на введення порожнього рядка. Функція gets () повертає рядок нульової довжини, якщо єдиною натиснутою клавішею виявилася клавіша < Enter >. Це означає, що першим байтом у рядку буде нульовий символ. Нульове значення завжди інтерпретується як помилкове, але узяте із запереченням (!) дає значення ІСТИНА, яке дає змогу виконати негайний вихід з циклу за допомогою настанови break.

5.6.2. Приклад використання масивів рядків

Масиви рядків зазвичай використовують для оброблення таблиць даних. Розглянемо, наприклад, спрощену базу даних службовців, у якій зберігається ім'я, номер телефону, кількість годин, відпрацьованих службовцями за звітний період, і розмір погодинного окладу для кожного службовця. Щоб створити таку програму для колективу, що складається з десяти службовців, визначимо чотири масиви (з них перші два будуть масивами рядків).

char name[10][80]; // Масив імен службовців.

char phone[10][20]; // Масив телефонних номерів службовців.

float hours[10]; // Масив годин, відпрацьованих за тиждень.

float oklad[10]; // Масив погодинних окладів.

Щоб ввести інформацію про кожного службовця, спробуємо скористатися функцією enter().

// Функція введення інформації в базу даних

void enter()

{

int i;

char tmp[80];

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

cout << "Введіть прізвище службовця: ";

cin >> name[i];

cout << "Введіть номер телефону службовця: ";

cin >> phone[i];

cout << "Введіть кількість відпрацьованих годин: ";

cin >> hours[i];

cout << "Введіть оклад службовця: ";

cin >> oklad[i];

}

}

На підставі введених даних можна скласти звіт, обчисливши заробітну плату, яка належить кожному службовцю. Для цього скористаємося такою функцією report():

// Відображення звіту

void report()

{

int i;

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

cout << name[i] << " " << phone[i] << "\n";

cout << "Заробітна плата за тиждень: "

<< oklad[i]*hours[i];

cout << "\n";

}

}

Повністю програму бази даних службовців наведено нижче. Зверніть особливу увагу на те, як реалізується доступ до кожного масиву. Ця версія програми ведення бази даних службовців ще далека від досконалості, оскільки введена в неї інформація втрачається відразу ж після виходу з програми. Трохи згодом Ви навчитеся зберігати інформацію в дисковому файлі.

Код програми 5.18. Демонстрація простої програми ведення бази даних службовців

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

 

char name[10][80]; // Масив імен службовців.

char phone[10][20]; // Масив телефонних номерів службовців.

float hours[10]; // Масив годин, відпрацьованих за тиждень.

float oklad[10]; // Масив погодинних окладів.

int menu_select();

void enter(), report();

 

int main ()

{

int vybir;

do {

vybir = menu_select(); // Отримуємо команду вибрану користувачем.

switch (vybir) {

case 0: break;

case 1: enter();

break;

case 2: report();

break;

default: cout << "Спробуйте ще раз.\n\n";

}

} while (vybir!= 0);

 

getch (); return 0;

}

 

// Функція повертає команду, вибрану користувачем.

int menu_select()

{

int vybir;

cout << "0. Вихід з програми\n";

cout << "1. Введення інформації\n";

cout << "2. Генерування звіту\n";

cout << "\n Виберіть команду: ";

cin >> vybir;

return vybir;

}

 

// Функція введення інформації в базу даних.

void enter()

{

int i;

char tmp[80];

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

cout << "Введіть прізвище службовця: ";

cin >> name[i];

cout << "Введіть номер телефону службовця: ";

cin >> phone[i];

cout << "Введіть кількість відпрацьованих годин: ";

cin >> hours[i];

cout << "Введіть оклад службовця: ";

cin >> oklad[i];

}

}

 

// Відображення звіту.

void report()

{

int i;

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

cout << name[i] << " " << phone[i] << "\n";

cout << "Заробітна плата за тиждень: "

<< oklad[i]*hours[i];

cout << "\n";

}

}


Розділ 6. Особливості застосування покажчиків

Покажчики, поза сумнівом, – один з найважливіших і складних аспектів мови програмування C++. Значною мірою потужність багатьох засобів мови програмування C++ визначається використанням покажчиків. Наприклад, завдяки ним забезпечується підтримка зв'язних списків і динамічного виділення пам'яті, саме вони дають змогу функціям змінювати значення своїх аргументів. Однак ці питання будуть розглядатися в подальших розділах, а поки що (тобто у цьому розділі) розглянемо основні особливості застосування покажчиків і продемонструємо, як можна уникнути деяких потенційних проблем, пов'язаних з їх використанням.

Під час розгляду основних понять про покажчики нам доведеться використовувати таке поняття як розмір базових С++-типів даних. Для цього запам'ятаємо тільки те, що символи займають в пам'яті один байт, цілочисельні значення – чотири, значення з плинною крапкою типу float – чотири, а значення з плинною крапкою типу double – вісім (ці розміри характерні для типового 32-розрядного середовища).

6.1. Основні поняття про покажчики

Покажчики – це змінні, які зберігають адреси пам'яті. Найчастіше ці адреси позначають місцезнаходження в пам'яті інших змінних. Наприклад, якщо х містить адресу змінної y, то про змінну х говорять, що вона "вказує" на y.

Покажчик – це змінна, яка містить|утримує| адресу іншої змінної.

Змінні-покажчики (або змінні типу покажчика)мають бути відповідно оголошені. Формат оголошення змінної-покажчика є таким:

тип *ім'я_змінної;

У цьому записі елемент тип означає базовий тип покажчика, причому він повинен бути допустимим С++-типом. Елемент ім'я_змінної є іменем змінної-покажчика.

Для розуміння сказаного розглянемо такий приклад. Щоб оголосити змінну р покажчиком на int -значення, використаємо таку настанову:

int *p;

Для оголошення покажчика на float -значення використаємо таку настанову:

float *p;

У загальному випадку використання символу "зірочка" (*) перед іменем змінної в настанові оголошення перетворює цю змінну на покажчик.

Базовий тип покажчика визначає тип даних, на які він посилатиметься.

Тип даних, на які посилатиметься покажчик, визначається його базовим типом. Розглянемо ще один приклад:

int *ip; // Покажчик на цілочисельне значення

double *dp; // Покажчик на значення типу double

Як зазначено в коментарях до цієї програми, змінна ip – це покажчик на int -значення, оскільки його базовим типом є тип int, а змінна dp – покажчик на double -значення, оскільки його базовим типом є тип double, Отже, в попередніх прикладах змінну ip можна використовувати для вказівки на int -значення, а змінну dp – на double -значення. Проте запам'ятайте: не існує реального засобу, який би зміг перешкодити покажчику посилатися на "казна-що". Ось через це покажчики потенційно небезпечні.

6.2. Використання|із| покажчиків з операторами присвоєння

6.2.1. Оператори роботи з покажчиками

З покажчиками використовуються два оператори: "*" і "&". Оператор "&" – унарний. Він повертає адресу пам'яті, за якою розташований його операнд[19]. Наприклад, у процесі виконання такого фрагмента коду програми

ptr = &balance;

у змінну ptr поміщається адреса змінної balance. Ця адреса відповідає області у внутрішній пам'яті комп'ютера, яка належить змінній balance. Виконання цієї настанови ніяк не впливає на значення змінної balance. Призначення оператора "&" можна "перекласти" українською мовою як "адреса змінної", перед якою він знаходиться. Отже, наведену вище настанову присвоєння можна виразити так: "змінна ptr отримує адресу змінної balance". Щоб краще зрозуміти суть цього присвоєння, припустимо, що змінна balance розташована у області пам'яті з адресою 100. Отже, після виконання цієї настанови змінна ptr набуде значення 100.

Другий оператор роботи з покажчиками (*) слугує доповненням до першого (&). Це також унарний оператор, але він звертається до значення змінної, розташованої за адресою, заданою його операндом. Іншими словами, він посилається на значення змінної, яка адресується заданим покажчиком. Якщо (продовжуючи роботу з попередньою настановою присвоєння) змінна ptr містить адресу змінної balance, то у процесі виконання настанови

value = *ptr;

змінній value буде присвоєне значення змінної balance, на яку вказує змінна ptr. Наприклад, якщо змінна balance містить значення 3200, то після виконання останньої настанови змінна value міститиме значення 3200, оскільки це якраз те значення, яке зберігається за адресою 100. Призначення оператора "*" можна виразити словосполученням "за адресою". У цьому випадку попередню настанову можна прочитати так: "змінна value набуває значення (розташоване) за адресою ptr". Дію наведених вище двох настанов схематично показано на рис. 6.1.

Рис. 6.1. Дія операторів "*" і "&"

Послідовність операцій, відображених на рис. 6.1, безпосередньо реалізується у наведеному нижче коді програми.

Код програми 6.1. Демонстрація механізму використання покажчиків з операторами

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

 

int main ()

{

int balance;

int *ptr;

int value;

 

balance = 3200;

ptr = &balance;

value = *ptr;

cout << "Баланс дорівнює: " << value << "\n";

 

getch (); return 0;

}

У процесі виконання ця програма відображає на екрані такі результати:

Баланс дорівнює: 3200

Шкода, але знак множення (*) і оператор із значенням "за адресою" позначаються однаковими символами "зірочка", що іноді збиває з пантелику новачків у мові програмування C++. Ці операції ніяк не пов'язані одна з іншою. Майте на увазі, що оператори "*" і "&" мають вищий пріоритет, ніж будь-який з арифметичних операторів, за винятком унарного мінуса, пріоритет якого такий самий, як у операторів, що вживаються для роботи з покажчиками.

Операції, що виконуються за допомогою покажчиків, часто називають операціями непрямого доступу, оскільки ми опосередковано отримуємо доступ до змінної за допомогою деякої іншої змінної.

Операція непрямого доступу – це процес використання покажчика для доступу до деякого об'єкта.

6.2.2. Важливість застосування базового типу покажчика

На прикладі попередньої програми було показано можливість присвоєння змінній value значення змінної balance за допомогою операції непрямого доступу, тобто з використанням покажчика. Можливо, при цьому у Вас промайнуло запитання: "Як С++-компілятор дізнається про те, скільки необхідно скопіювати байтів у змінну value з області пам'яті, яка адресується покажчиком ptr?". Сформулюємо те саме запитання в більш загальному вигляді: як С++-компілятор передає належну кількість байтів у процесі виконання операції присвоєння з використанням покажчика? Відповідь звучить так. Тип даних, який адресується покажчиком, вважається базовим типом покажчика. Оскільки ptr є покажчиком на цілочисельний тип, то у цьому випадку С++-компілятор скопіює в змінну value з області пам'яті, яка адресується покажчиком ptr, чотири байти інформації (що справедливе для 32-розрядного середовища). Але якби ми мали справу з double -покажчиком, то в аналогічній ситуації скопіювалося б вісім байтів інформації.

Змінні-покажчики повинні завжди вказувати на відповідний тип даних. Наприклад, під час оголошення покажчика типу int компілятор "припускає", що всі значення, на які посилається цей покажчик, мають тип int. С++-компілятор просто не дасть змоги виконати операцію присвоєння за участю покажчиків (з обох боків від оператора присвоєння), якщо типи цих покажчиків несумісні (по суті не однакові).

Наприклад, такий фрагмент коду програми є некоректним:

int *p;

double f;

//... р = &f; // Помилка!

Некоректність цього фрагмента полягає в неприпустимості присвоєння double -покажчика int -покажчику. Вираз &f генерує покажчик на double -значення, а р – покажчик на цілочисельний тип int. Ці два типи несумісні, тому компілятор відзначить цю настанову як помилкову і не скомпілює програму.

Як було заявлено вище, під час присвоєння два покажчики мають бути сумісні за типом, проте це серйозне обмеження можна подолати (правда, на свій страх і ризик) за допомогою операції приведення типів. Наприклад, такий фрагмент коду програми тепер формально є більш коректним:

int *p;

double f;

//...

p = (int *) &f; // Тепер формальне все ОК|!

Операція наведення до типу (int *) викличе перетворення double - до int -покажчика. Все ж таки використання операції приведення з цією метою є дещо сумнівним, оскільки саме базовий тип покажчика визначає, як компілятор поводитиметься з даними, на які він посилається. У цьому випадку, незважаючи на те, що р (після виконання останньої настанови) насправді вказує на значення з плинною крапкою, компілятор, як і раніше, "вважає", що він вказує на цілочисельне значення (оскільки р за визначенням – int -покажчик).

Щоб краще зрозуміти, чому використання операції приведення типів під час присвоєння одного покажчика іншому не завжди є допустимим, розглянемо таку програму.

Код програми 6.2. Демонстрація неправильного написання коду програми

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

 

int main ()

{

double x, y;

int *p;

x = 123.23;

p = (int *) &x; // Використовуємо операцію приведення типів

// для присвоєння double -покажчика int -покажчику.

y = *p; // Що відбувається у процесі виконання цієї настанови?

cout << y; // Що виведе ця настанова?

 

getch (); return 0;

}

Як бачимо, у цій програмі змінній р (точніше, покажчику на цілочисельне значення) присвоюється адреса змінної х (яка має тип double). Отже, коли змінній y присвоюється значення, яка адресується покажчиком р, то змінна y отримує тільки чотири байти даних (а не всі вісім, що є потрібними для double -значення), оскільки р – покажчик на цілочисельний тип int. Таким чином, у процесі виконання cout -настанови на екран буде виведено не число 123.23, а, висловлюючись мовою програмістів, "сміття"[20], яке насправді є молодшими 4-ма байтами мантиси.

6.2.3. Присвоєння значень за допомогою покажчиків

Під час присвоєння значення області пам'яті, яка адресується покажчиком, його (покажчик) можна використовувати з лівого боку від оператора присвоєння. Наприклад, у процесі виконання такої настанови (якщо р – покажчик на цілочисельний тип)

*p = 101;

число 101 присвоюється області пам'яті, в р, яка адресується покажчиком, Таким чином, цю настанову можна прочитати так: "за адресою р поміщаємо значення 101". Щоб інкрементувати або декрементувати значення, розташоване у області пам'яті, яка адресується покажчиком, можна використовувати настанову, подібну до такої:

(*p)++;

Круглі дужки тут є обов'язковими, оскільки оператор "*" має нижчий пріоритет, ніж оператор "++".

Присвоєння значень з використанням покажчиків продемонстровано у наведеному нижче коді програми.

Код програми 6.3. Демонстрація механізму присвоєння значень з використанням покажчиків

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

 

int main ()

{

int *p, num;

p = &num;

*p = 100;

cout << num << " ";

(*p)++;

cout << num << " ";

(*p)--;

cout << num << "\n";

 

getch (); return 0;

}

Ось такі результати генерує ця програма.

100 101 100

6.3. Використання покажчиків у виразах

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

6.3.1. Арифметичні операції над покажчиками

З покажчиками можна використовувати тільки чотири арифметичних оператори: ++; --; + і -. Щоб краще зрозуміти, що відбувається у процесі виконання арифметичних дій з покажчиками, почнемо з конкретного прикладу. Нехай р1 – покажчик на int -змінну з поточним значенням 2 000 (тобто р1 містить адресу 2 000). Після виконання (у 32-розрядному середовищі) виразу

p1++;

вміст змінної-покажчика р1 дорівнюватиме 2 004, а не 2 001! Йдеться про те, що під час кожного інкрементування покажчик р1 вказуватиме на наступне int -значення. Для операції декрементування справедливе зворотне твердження, тобто під час кожного декрементування значення р1 зменшуватиметься на 4. Наприклад, після виконання настанови

p1--;

покажчик р1 матиме значення 1 996, якщо до цього воно дорівнювало 2 000. Отже, кожного разу, коли покажчик інкрементується, він вказуватиме на область пам'яті, що містить наступний елемент базового типу цього покажчика. А під час кожного декрементування він вказуватиме на область пам'яті, що містить попередній елемент базового типу цього покажчика.

Для покажчиків на символьні значення результат операцій інкрементування і декрементування буде таким самим, як при "нормальній" арифметиці, оскільки символи займають тільки один байт. Але під час використання будь-якого іншого типу покажчика у процесі інкрементування або декрементування значення змінної-покажчика збільшуватиметься або зменшуватиметься на величину, що дорівнює розміру його базового типу.

Арифметичні операції над покажчиками не обмежуються використанням операторів інкремента і декремента. Із значеннями покажчиків можна виконувати операції додавання і віднімання, використовуючи як другий операнд цілочисельні значення. Вираз

р1 = р1 + 9;

примушує р1 посилатися на дев'ятий елемент базового типу покажчика р1 щодо елемента, на який р1 посилався до виконання цієї настанови.

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




Поделиться с друзьями:


Дата добавления: 2014-11-29; Просмотров: 507; Нарушение авторских прав?; Мы поможем в написании вашей работы!


Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет



studopedia.su - Студопедия (2013 - 2024) год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! Последнее добавление




Генерация страницы за: 0.17 сек.