Студопедия

КАТЕГОРИИ:


Архитектура-(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 11 страница




cout << myFunc(10.1) << " ";

не виникає ніякої неоднозначності: компілятор мови C++ "впевнено" забезпечує виклик функції myFunc(double), оскільки, якщо не задано безпосередньо інше, всі літерали з плинною крапкою у мові програмування C++ автоматично отримують тип double. Але під час виклику функції myFunc() з аргументом, що дорівнює цілому числу 10, у програму вноситься неоднозначність, оскільки компіляторові невідомо, в який тип йому необхідно перетворити цей аргумент: float або double. Обидва перетворення є допустимими. У такій неоднозначній ситуації буде видано повідомлення про помилку, і програма не скомпілюється. На прикладі попередньої програми хотілося б наголосити, що неоднозначність в ній викликана не перевантаженням функції myFunc(), оголошеної двічі для прийому double - і float -аргументу, а використанням при конкретному виклику функції myFunc() аргументу невизначеного для перетворення типу. Іншими словами, помилка полягає не в перевантаженні функції myFunc(), а в конкретному її виклику.

А ось ще один приклад неоднозначності, викликаної автоматичним перетворенням типів у мові програмування C++.

Код програми 8.14. Демонстрація помилки, спричиненої неоднозначністю

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

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

char myFunc(unsigned char ch);

char myFunc(char ch);

 

int main ()

{

cout << myFunc('с'|); // Тут викликається myFunc(char).

cout << myFunc(88) << " "; // Вноситься неоднозначність.

 

getch (); return 0;

}

 

char myFunc(unsigned char ch)

{

return ch-1;

}

 

char myFunc(char ch)

{

return ch+1;

}

У мові програмування C++ типи unsigned char і char не є істотно неоднозначними, позаяк це різні типи. Але під час виклику функції myFunc() з цілочисельним аргументом 88 компілятор "не знає", яку функцію йому виконати, тобто в значення якого типу йому необхідно перетворити число 88: типу char або типу unsigned char? Обидва перетворення тут цілком допускаються.

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

Код програми 8.15. Демонстрація ще одного прикладу неоднозначності

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

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

int myFunc(int izm);

int myFunc(int izm, int jzm=1);

 

int main ()

{

cout << myFunc(4, 5) << " "; // Неоднозначності немає

cout << myFunc(10); // Виникнення неоднозначності

 

getch (); return 0;

}

 

int myFunc(int izm)

{

return izm;

}

 

int myFunc(int izm, int jzm)

{

return izm*jzm;

}

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

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


Розділ 9. С++-специфікатори та спеціальні оператори

Перш ніж переходити до перегляду складніших засобів мови програмування C++, є сенс грунтовніше познайомитися з С++-специфікаторами типів даних і класів пам'яті, а також з деякими спеціальними операторами. Окрім вже розглянутих вище типів даних, y мові програмування C++ визначено ще й інші. Одні з них складаються з модифікаторів, що додаються до вже відомих типів, інші містять перерахунки, а треті використовують ключове слово typedef. Мова C++ також підтримує ряд операторів, які значно розширюють область дії мови і дають змогу розв'язувати задачі програмування у надзвичайно широкому діапазоні. Йдеться про порозрядні оператори, оператори зсуву, а також оператори "? " і sizeof. Окрім того, у цьому розділі розглядаються такі спеціальні оператори, як new і delete. Вони призначені для підтримки С++-системи динамічного розподілу пам'яті.

9.1. Специфікатори типів даних

Специфікатори типів даних const і volatile керують доступом до змінної.

У мові програмування C++ визначено два специфікатори типів даних – const і volatile, котрі роблять вплив на те, як можна отримати доступ до змінних або модифікувати їх. Офіційно їх іменують cv -специфікаторами і вони мають передувати базовому типу під час оголошення змінної.

9.1.1. Застосування специфікатора типу даних const

Змінні, які оголошено з використанням специфікатора типу даних const, не можуть змінити свої значення у процесі виконання програми. Проте будь-якій const -змінній можна присвоїти деяке початкове значення. Наприклад, у процесі виконання такої настанови

const double vzm = 3.2;

створюється double -змінна vzm, яка містить значення 3.2, і це значення у процесі виконання програми змінити вже не можна. Але цю змінну можна використовувати в інших виразах. Будь-яка const -змінна набуває значення або під час ініціалізації, що задається безпосередньо, або під час використання апаратно-залежних засобів. Застосування специфікатора типу даних const до оголошення змінної гарантує, що вона не буде модифікована іншими частинами Вашої про­грами.

Специфікатор типу даних const запобігає модифікації змінної у процесі виконання програми.

Специфікатор типу даних const має ряд важливих застосувань. Можливо, частіше за все його використовують для створення const -параметрів типу покажчика. Такий параметр-по­каж­чик захищає об'єкт, на який він посилається, від модифікації з боку функції. Іншими словами, якщо параметр-покажчик передує ключовому слову const, то ніяка настанова цієї функції не може модифікувати змінну, яка адресується цим параметром. Наприклад, функція code() у наведеній нижче короткій програмі зсовує кожну букву в повідомленні на одну алфавітну позицію (тобто замість букви "А" ставиться буква "Б" і т.д.), відображаючи таким чином повідомлення в закодованому вигляді. Використання специфікатора типу даних const в оголошенні параметра не дає змоги коду функції модифікувати об'єкт, на який вказує цей параметр.

Код програми 9.1. Демонстрація зсуву букв у повідомленні на одну алфавітну позицію

#include <vcl>

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

#include <conio> // Для консольного режиму роботи

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

 

void code(const char *str);

 

int main ()

{

code("Це тест."); // Зсув букви на одну алфавітну позицію

 

getch (); return 0;

}

 

/* Використання специфікатора типу даних const гарантує, що str

не може змінити аргумент, на який він вказує. */

void code(const char *str)

{

while (*str) {

cout << (char) (*str+1);

str++;

}

}

Оскільки параметр str оголошується як const -покажчик, то у функції code() немає ніякої можливості внести зміни в рядок, яка адресується покажчиком str. Але, якщо Ви спробуєте написати функцію code() так, як це показано в наведеному нижче прикладі, то обов'язково отримаєте повідомлення про помилку і програма не скомпілюється.

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

void code(const char *str)

{

while (*str) {

*str = *str + 1; // Помилка, аргумент модифікувати не можна

cout << (char) *str;

str++;

}

}

Оскільки параметр str є const -покажчиком, то його не можна використовувати для модифікації об'єкта, на який він посилається.

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

Код програми 9.2. Демонстрація неможливості модифікувати const-посилання

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

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

void fun(const int &izm);

 

int main ()

{

int kzm = 10;

fun(kzm);

 

getch (); return 0;

}

 

// Використання посилального const -параметра.

void fun(const int &izm)

{

izm = 100; // Помилка, не можна модифікувати const -посилання.

cout << izm;

}

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

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

// Використання специфікатора типу даних const

// для створення іменованих констант

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

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

const int size = 10;

 

int main ()

{

int A1[size], A2[size], A3[size];

//...

}

Якщо у наведеному прикладі знадобиться використовувати новий розмір для масивів, то Вам потрібно буде змінити тільки оголошення змінної size і перекомпілювати програму. Як наслідок, всі три масиви автоматично отримають новий розмір.

9.1.2. Застосування специфікатора типу даних volatile

Специфікатор типу даних volatile повідомляє компіляторові про те, що значення відповідної змінної може бути змінене у програмі опосередковано. Наприклад, адреса певної глобальної змінної може передаватися керованій змінній перериваннями підпрограмі трактування, яка обновляє цю змінну з приходом кожного імпульсу сигналу часу. У такій ситуації вміст змінної змінюється без використання безпосередньо заданих настанов програми. Існують вагомі підстави для того, щоб повідомити компілятор про зовнішні чинники зміни змінної. Йдеться про те, що С++-компіляторові дозволяється автоматично оптимізувати певні вирази в припущенні, що вміст тієї або іншої змінної залишається незмінним, якщо воно не знаходиться в лівій частині настанови присвоєння. Але, якщо деякі чинники (зовнішні стосовно програми) змінять значення цього поля, то таке припущення виявиться неправильним, внаслідок чого можуть виникнути проблеми.

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

Наприклад, у наведеному нижче коді програми припустимо, що змінна chas обновляється кожну мілісекунду годинниковим механізмом комп'ютера. Але, оскільки змінна chas не оголошена з використанням специфікатора типу даних volatile, цей фрагмент коду програми може іноді працювати неналежним чином. Зверніть особливу увагу на рядки, позначені буквами "А" і "Б":

int chas, timerClass;

//...

 

timerClass = chas; // Рядок А

//... Виконання будь-яких дій.

cout << "Пройдений час " << chas-timerClass; // Рядок Б

У цьому коді програми змінна chas набуває свого значення, коли вона присвоюється змінній timerClass у рядку А. Але, оскільки змінна chas не оголошена з використанням специфікатора типу даних volatile, то компілятор має можливість оптимізувати цей код, причому у такий спосіб, при якому значення змінної chas, можливо, не буде запитано в настанові cout (рядок Б), якщо між рядками А і Б не буде жодного проміжного присвоєння значення змінної chas. Іншими словами, у рядку Б компілятор може просто ще раз використовувати значення, яке отримала змінна chas у рядку А. Але, якщо між моментами виконання рядків А і Б надійдуть чергові імпульси сигналу часу, то значення змінної chas обов'язково зміниться, а рядок Б у цьому випадку не відобразить коректного результату.

Для вирішення цього питання необхідно визначити змінну chas з ключовим словом volatile:

volatile int chas;

Тепер значення змінної chas запитуватиметься під час кожного її використанні. І хоча на перший погляд це може видатися дивним, проте специфікатори типів даних const і volatile можна використовувати разом. Наприклад, наведене нижче оголошення абсолютно допустиме. Воно створює const -покажчик на volatile-об'єкт:

const volatile unsigned char *port = (const volatile char *) 0x2112;

У наведеному прикладі для перетворення цілочисельного літерала 0x2112 в const -покажчик на volatile-символ необхідно застосувати операцію приведення типів.

9.2. Специфікатори класів пам'яті

Мова програмування C++ підтримує п'ять специфікаторів класів пам'яті:

За допомогою цих ключових слів компілятор отримує інформацію про те, як повинна зберігатися змінна. Специфікатор класів пам'яті необхідно вказувати на початку оголошення змінної.

Специфікатори класів пам'яті визначають, як повинна зберігатися змінна.

Специфікатор mutable застосовується тільки до об'єктів класів, про які йтиметься попереду. Решту специфікаторів ми розглянемо у цьому розділі.

9.2.1. Застосування специфікатора класу пам'яті auto

Специфікатор класу пам'яті auto оголошує локальну змінну. Але він використовується досить рідко (можливо, Вам ніколи і не доведеться застосувати його), оскільки локальні змінні є "автоматичними" за замовчуванням. Навряд чи Вам трапиться це ключове слово і в чужих програмах.

Рідко використовуваний специфікатор класу пам'яті auto оголошує локальну змінну.

9.2.2. Застосування специфікатора класу пам'яті extern

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

Специфікатор класу пам'яті extern оголошує змінну, але не виділяє для неї області пам'яті.

У програмах, які складаються з двох або більше файлів, кожен файл повинен "знати" імена і типи глобальних змінних, що використовуються програмою в цілому. Проте не можна просто оголосити копії глобальних змінних у кожному файлі. Йдеться про те, що у мові програмування C++ програма може містити тільки одну копію кожної глобальної змінної. Отже, якщо Ви спробуєте оголосити необхідні глобальні змінні у кожному файлі, то виникнуть проблеми. Коли компонувальник спробує скомпонувати ці файли, то він виявить дубльовані глобальні змінні, і компонування програми не відбудеться. Щоб вийти з цього скрутного становища, достатньо| оголосити всі глобальні змінні в одному файлі, а в інших використовувати extern-оголошення, як це показано в табл. 9.1.

Табл. 9.1. Використання глобальних змінних в окремо компільованих модулях

Файл F1 Файл F2
int x, y; char ch; int main() { //... } void func1() { x = 123; } extern int x, y; extern char ch; void func22() { x = y/10; } void func23() { y = 10; }

У файлі F1 оголошуються та визначаються змінні х, y і ch. У файлі F2 використовується скопійований з файлу F1 перелік глобальних змінних, до оголошення яких додано ключове слово extern. Специфікатор класу пам'яті extern робить змінну відомою для модуля, але насправді не створює її. Іншими словами, ключове слово extern надає компіляторові інформацію про тип і ім'я глобальних змінних, повторно не виділяючи для них пам'яті. Під час компонування цих двох модулів усі посилання на ці зовнішні змінні будуть визначені.

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

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

Для розуміння сказаного розглянемо такий приклад. Звернемо Вашу увагу на те, що глобальні змінні first і last оголошуються не перед, а після функції main ().

Код програми 9.3.

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

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

 

int main ()

{

extern int first, last; // Використання глобальних змінних.

cout << first << " " << last << "\n";

 

getch (); return 0;

}

 

// Глобальне визначення змінних first і last.

int first = 10, last = 20;

У процесі виконання цієї програми на екран буде виведено числа 10 20, оскільки глобальні змінні first і last, що використовуються в настанові cout, ініціалізувалися цими значеннями. Оскільки extern -оголошення у функції main () повідомляє компіляторові про те, що змінні first і last оголошуються десь у іншому місці (у цьому випадку нижче, але у тому ж файлі), програму можна скомпілювати без помилок, незважаючи на те, що змінні first і last використовуються до їх визначення.

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

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

Варто знати! Незважаючи на те, що специфікатор класу пам'яті extern оголошує, але не визначає змінну, то існує один виняток з цього правила. Якщо в extern -визначенні змінна ініціалізувалася, то таке extern -оголошення стає визначенням. Це дуже важливий момент, оскільки будь-який об'єкт може мати декілька оголошень, але тільки одне визначення.

9.2.3. Статичні змінні

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

Локальні static-змінні

Якщо до локальної змінної застосовано модифікатор static, то для неї виділяють постійну область пам'яті практично так само, як і для глобальної змінної. Це дає змогу статичній змінній підтримувати її значення між викликами функцій. Іншими словами, на відміну від звичайної локальної змінної, значення static-змінної не втрачається при виході з функції. Ключова відмінність між статичною локальною і глобальною змінними полягає у тому, що статична локальна змінна відома тільки блоку, у якому вона оголошена. Таким чином, статичну локальну змінну деякою мірою можна назвати глобальною змінною, яка має обмежену область видимості.

Локальна static -змінна підтримує своє значення між викликами функції.

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

static int count;

Статичній змінній можна присвоїти початкове значення. Наприклад, у цій настанові змінній count присвоюється початкове значення 200:

static int count = 200;

Локальні static -змінні ініціалізувалися тільки одного разу, на початку виконання програми, а не під час кожного вході у функцію, у якій вони оголошені.

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

Розглянемо приклад використання модифікатора static -змінної. Вона слугує для зберігання поточного середнього значення від чисел, що вводяться користувачем.

Код програми 9.4. Демонстрація механізму використання локальних static-змінних

#include <vcl>

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

#include <conio> // Для консольного режиму роботи

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

 

int srZnach(int izm); // Обчислення поточного середнього значення.

 

int main ()

{

int num;

 

do {

cout << "Введіть число (-1 означає вихід): ";

cin >> num;

if (num!= -1)

cout << "Поточне середнє дорівнює: " << srZnach(num);

cout << "\n";

} while (num > -1);

 

getch (); return 0;

}

 

int srZnach(int izm) // Обчислення поточного середнього значення.

{

static int sum = 0, count = 0;

sum = sum + izm;

count++;

 

return sum / count;

}

У цій програмі обидві локальні змінні sumі count оголошено статичними, тобто вони були ініціалізовані значенням 0.

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

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

Глобальні static-змінні

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

Глобальна static -змінна відома тільки для файлу, у якому вона оголошена.

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




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


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


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



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




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