ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 21.12.2021
Просмотров: 972
Скачиваний: 3
Слід застерегти від використання директиви препроцесора #define від використання в цілях подібних до typedef.
Приклад 16. Заміна typedef за допомогою директиви #define
#define char* PCHAR
typedef char* pchar;
void f(void)
{
PCHAR pc1,pc2;/* УВАГА! Еквівалентно char* pc1,pc2; */
pchar pc3,pc4;/* Еквівалентно char *pc3,*pc4 */
/* … */
}
Припустимо нам необхідно об’явити чотири змінних типу вказівників на символ. На перший погляд ідентичні записи із прикладу 15 на практиці призводять до дещо неочікуваних результатів. Із синтаксичної точки зору програма немає помилок і може бути успішно скомпільована, але за рахунок того що всі макропідстановки виконуються препроцесором ще до етапу компіляції і представляють собою просту заміну літералу PCHAR на char*, об’явлення змінної pc2 трактується компілятором як об’явлення змінної типу char.
7.3 Об’єднання
Об’єднання (union) – сукупність зв’язаних даних різних типів, для зберігання яких у пам’яті ЕОМ використовуються спільні копірки.
Синтаксис визначення об’єднання:
union <назва об’єднання>
{
<опис елементів>
};
Об'єднання використовуються коли необхідно отримати доступ до одних і тих же даних різними способами, або для економії пам’яті ЕОМ у випадку коли виключена можливість одночасного використання різних елементів об’єднання.
Оскільки для зберігання усіх елементів об’єднання використовується спільна область пам’яті, розмір змінної типу об’єднання є розміром найбільшого елементу, об’явленого в даному об’єднанні, на відміну від змінної типу структури, у якої розмір використовуваної пам’яті є сумою розмірів усіх елементів структури.
Приклад 17. Порівняння розмірів об’єднання і структури.
#include <stdio.h>
#include <stdlib.h>
union un
{
int ivar;
int* pivar;
char pstr[8];
float fvar;
long int livar;
};
struct st
{
int ivar;
int* pivar;
char pstr[8];
float fvar;
long int livar;
};
int main(void)
{
printf("Union size:\t%d\n", sizeof(union un));
printf("Struct size:\t%d\n", sizeof(struct st));
system("pause");
return 0;
}
Результат роботи програми:
Як бачимо з програмного коду прикладу 17, найбільший розмір і у структурі і у об’єднанні займає елемент pstr[8] (8 байт). У випадку об’єднання розмір цього елементу і визначає розмір змінної типу об’єднання.
Синтаксично робота зі змінними типу об’єднання аналогічна роботі зі змінними типу структур. Але синтаксис опису змінних типу об’єднання відрізняється:
union <назва об’єднання> < ім’я змінної>;
Як і у випадку зі структурами, використання ключового слова union є обов’язковим. Також дозволяється описувати змінні типу об’єднання одразу після визначення об’єднання.
Приклад 18. Робота з об’єднаннями.
#include <stdio.h>
#include <stdlib.h>
union un
{
int ivar;
char pstr[8];
};
int main(void)
{
union un un_var;
un_var.ivar = 0;
printf("un_var.ivar=%d\n",un_var.ivar);
printf("un_var.pstr>> ");
scanf("%s", un_var.pstr);
printf("un_var.ivar=%d\n", un_var.ivar);
printf("un_var.ivar>> ");
scanf("%d", &un_var.ivar);
printf("un_var.pstr=%s\n", un_var.pstr);
system("PAUSE");
return 0;
}
Результат роботи програми:
Приклад 18 демонструє спільне використання однієї області пам’яті декількома різними об’єктами об’єднання. Графічне представлення образу змінної un_var у пам’яті показано на рисунку 7.2.
Рисунок 7.2 – Змінна типу об’єднання у пам’яті.
Подібне використання пам’яті, окрім економії останньої, надає змогу зручного доступу до однакових даних різними способами, а також дозволяє проводити нестандартні приведення типів даних.
Приклад 19. Приклад організації доступу до однієї області пам’яті різними способами за допомогою об’єднань.
#include <stdio.h>
#include <stdlib.h>
union dig
{
struct
{
int digit0;
int digit1;
int digit2;
int digit3;
} digits;
int digit[4];
};
int main(void)
{
union dig data;
int i;
printf("Vvedit' 4 chisla>> \n");
scanf("%d", &data.digits.digit0);
scanf("%d", &data.digits.digit1);
scanf("%d", &data.digits.digit2);
scanf("%d", &data.digits.digit3);
printf("Vi vvely taki chisla:\n");
for(i=0; i<4; i++)
printf("\t%d) %d\n", i+1, data.digit[i]);
system("PAUSE");
return 0;
}
Результат роботи програми:
Об’єднання, аналогічно структурам, можуть бути неіменованими. Такі об’єднання називаються анонімними.
Перевизначення об’єднаннь в межах однієї області видимості не дозволяються, за виключенням випадку коли перевизначення здійснюється в різних програмних блоках. У випадку визначення об’єднання у окремому програмному блоці, область видимості цього визначення закінчується з закінченням програмного блоку, в якому міститься це визначення.
Приклад 20. Області видимості визначень об’єднань.
union un /* Глобально-видиме визначення */
{
int ivar;
char pstr[8];
};
void func(void)
{
union un; /* Визначення, яке видиме тільки в межах
функції func */
{
int ivar;
char pstr[8];
float fvar;
};
/* ... */
}
/* ... */
Код, наведений у прикладі 20 є коректним. Але слід застерегти від подібного роду перевизначень, що суттєво ускладнюють розуміння і супроводження програм, написаних у такому стилі.
7.4 Перераховуваний тип (Enum)
Enum (перерахування) – тип даних що має обмежений список ідентифікаторів.
Кожному значенню відповідає власне ім'я-ідентифікатор і ціле число, значення цього імені.
Синтаксис визначення перерахувань:
enum <ідентифікатор типу>
{
<список перерахування>
};
Тут ідентифікатор типу задає ім'я перераховуваного типу. Список перерахування складається з імен, розділених комами. Кожне ім’я задається ідентифікатором значення і, можливо, цілим значенням типу char чи int.
Приклад **1. Визначення перерахувань.
enum dataType
{
DT_INT, /* 0 */
DT_CHAR, /* 1 */
DT_STRING=3, /* 3 */
DT_FLOAT, /* 4 */
DT_BYTE=8, /* 8 */
DT_SHORT, /* 9 */
DT_POINTER /* 10 */
};
Коли не вказано значення ідентифікатора, воно буде на одиницю більшим за значення попереднього ідентифікатора. Значення за замовчанням для першого ідентифікатора у списку перерахування нуль.
Для зберігання значень змінних перерахування використовується тип int.
Перерахування зручно використовувати для запобігання появ у коді програми числових літералів, логічне значення яких може бути незрозуміле читачеві. Також перерахування це більш безпечна заміна макровизначенням #define.
Синтаксис оголошення змінної типу перерахування:
enum < ідентифікатор типу перерахування> < ім’я змінної>;
Приклад **2. Використання перерахувань.
#include <stdio.h>
#include <stdlib.h>
enum day
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
void prn_day(enum day d) /* перерахування як аргумент функції */
{
static char week[][10]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"};
printf("%s\n",week[d]); /* d приводиться до типу int */
}
int main(void)
{
enum day cnt; /* оголошення змінної типу перерахування */
for(cnt=Sunday;cnt<Saturday;++cnt)
prn_day(cnt);
system("PAUSE");
return 0;
}
Результат роботи програми:
Областю видимості типу перерахування є програмний блок, в якому було визначено перерахування. Область видимості може бут глобальною.
Визначення перерахувань може не містити імені. Доступ до елементів перерахування можливо здійснювати без оголошення змінної типу перерахування.
Приклад **3. Доступ до елементів неіменованого перерахування.
#include <stdio.h>
#include <stdlib.h>
enum
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
void prn_day(int d)
{
static char week[][10]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"};
printf("%s\n",week[d]);
}
int main(void)
{
int i;
for(i=Sunday;i<Saturday;++i)
prn_day(i);
system("PAUSE");
return 0;
}
Програма приведена у прикладі **3 працює аналогічно програмі з прикладу **2. Відмінність становить метод використання перерахувань. Якщо у прикладі **2 перерахування використовувалось як власний тип даних, що підкреслював логічну структуру програми, то у прикладі **3 перерахування служить тільки для введення іменованих констант. Таке використання перерахувань аналогічне введеню макровизначень за допомогою директиви препроцесора #define.
Приклад **3 показує загальнодоступність ідентифікаторів значень списку перерахування. Це в свою чергу означає що два різних визначення перераховувального типу в одному програмному блоці не можуть мати ідентифікаторів значень з однаковими іменами, навіть якщо їм присвоєно однакові значення.
11 ДИНАМІЧНЕ ВИДІЛЕННЯ ПАМ'ЯТІ
Динамічне виділення пам'яті - спосіб виділення оперативної пам'яті комп'ютера для об'єктів у програмі, при якому виділення пам'яті під об'єкт здійснюється під час виконання програми.
При динамічному розподілі пам'яті об'єкти розміщуються в т.зв. «Купі» (англ. heap- набір «вільних» блоків пам’яті): при конструюванні об'єкта вказується розмір запитуваної під об'єкт пам'яті, і, в разі успіху, виділена область пам'яті, умовно кажучи, «вилучається» з «купи», стаючи недоступною при подальших операціях виділення пам'яті. Протилежна за змістом операція - звільнення зайнятої раніше під який-небудь об'єкт пам'яті: звільняється пам'ять, також умовно кажучи, повертається в «купу» і стає доступною при подальших операцій виділення пам'яті.
Рисунок *.1 – Схематичне зображення «купи».
У міру створення в програмі нових об'єктів, кількість доступної пам'яті зменшується. Звідси випливає необхідність постійно звільняти раніше виділену пам'ять. В ідеальній ситуації програма повинна повністю звільнити всю пам'ять, яка потрібна для роботи. За аналогією з цим, кожна процедура (функція або підпрограма) повинна забезпечити звільнення всієї пам'яті, виділеної під час виконання процедури. Некоректний розподіл пам'яті приводить до т.з. «витоку» ресурсів, коли виділена пам'ять не звільняється. Численні витоки пам'яті можуть призвести до вичерпання всієї оперативної пам'яті і порушити роботу операційної системи.
Інша проблема - це проблема фрагментації пам'яті. Виділення пам'яті відбувається блоками - безперервними фрагментами оперативної пам'яті (таким чином, кожен блок - це кілька байтів що йдуть підряд ). У якийсь момент, в купі просто може не виявитися блоку відповідного розміру і, навіть, якщо вільної пам'яті достатньо для розміщення об'єкта, операція виділення пам'яті закінчиться невдачею.
Наприклад нам потрібна пам'ять під масив розміром 6 блоків (Рисунок *.2). У нас вільних всього 8 блоків. Як відомо масив – це послідовний набір однотипних даних, тому нам потрібно послідовно 6 блоків. Як видно з рисунка, ми не можемо виділити цю пам'ять, хоча к-сть блоків для цього є достатньою, тому операція закінчиться невдачею.
Рисунок *.2 – Демонстрація виділення памяті
Для керування динамічним розподілом пам'яті використовується «збирач сміття» - програмний об'єкт, який стежить за виділенням пам'яті і забезпечує її своєчасне звільнення. Збирач сміття також стежить за тим, щоб вільні блоки мали максимальний розмір, і, при необхідності, здійснює дефрагментцію пам'яті (Рисунок*.3).
Рисунок *.3 – Робота дефрагментатора
Для роботи з динамічним виділенням пам’яті використовують функції malloc і calloc, за допомогою яких і відбувається виділення пам’яті, функції realloc, що дозволяє змінювати розмір виділеної ділянки пам’яті. Обовязково з цими ф-ями повинна використовуватись функція free, що звільняє виділену пам'ять.
Функція malloc
Прототип:
void * malloc ( size_t size );
Виділяє блок розміром size байт в пам'яті, повертаючи вказівник на початок блоку. Цей вказівник завжди має тип void, і може бути приведений до необхідного типу даних при розіменовуванні. Якщо функції не вдалося виділити необхідний блок пам'яті, повертається вказівник на NULL.
Виділений блок пам'яті не ініціалізований, тобто залишається з невизначеними значеннями.
Приклад застосування функції malloc, програма №1:
#include <stdio.h>
#include <stdlib.h>
int main (void)
{
int vari;
char * string;
int num;
printf ("Dovzhina strichky: ");
scanf ("%d", &vari);
string = (char*) malloc (vari+1);
if (string ==NULL)
exit (1);
for (num=0; num <vari; num ++)
string[num]=rand()%vari +'a';
string[vari]='\0';
printf ("Strichka: %s\n",string);
free (string);
system("PAUSE");
return 0;
}
Результат роботи програми:
Ця програма генерує рядок довжиною, яка зазначена користувачем (змінна vari), і заповнює його випадковими символами. Можлива довжина цього рядка обмежена лише кількістю вільної пам'яті в системі, яку malloc може виділити.
Функція calloc
Прототип:
void * calloc ( size_t num, size_t size );
Виділяє блок пам'яті для масиву з num елементів, кожен з яких має розмір size байт, і ініціалізує всі комірки виділеної пам’яті нулями. В результаті буде виділена пам'ять розміром size*num байт.
У разі успіху повертається вказівник на блок виділеної пам'яті. Цей вказівник завжди має тип void, і може бути приведений до необхідного типу даних при розіменовуванні. Якщо функції не вдалося виділити необхідний блок пам'яті, повертається вказівник на NULL.
Приклад застосування функції calloc, програма №2:
#include <stdio.h>
#include <stdlib.h>
int main (void)
{
int vari, num;
int * p;
printf ("Kilkist' elementiv masyvu: ");
scanf ("%d",&vari);
p = (int*) calloc (vari,sizeof(int));
if (p == NULL)
exit (1);
for (num = 0;num < vari; ++num)
{
printf ("Vvedit' chyslo pid nomerom #%d: ",num);
scanf ("%d",&p[num]);
}
printf ("Vy vvely: ");
for (num = 0;num < vari; ++num)
printf ("%d ",p[num]);
free (p);
system("PAUSE");
return 0;
}
Результат роботи програми:
Слід зазначити що функція calloc потребує для свого виконання більше часу,оскільки проводить ініціалізацію виділеної пам’яті. У випадках, коли є критичною швидкодія програми, доцільно користуватися функцією malloc.
Здійснити заміну функції calloc на malloc можна переписавши стрічку :
p = (int*) calloc (vari,sizeof(int));
на
p = (int*) malloc (vari*sizeof(int));
Функція realloc
Прототип:
void * realloc (void *block, size_t size);
Змінює розмір блоку block, який раніше був виділений за допомогою функцій malloc або calloc. Аргумент size задає новий розмір блоку. Вміст блоку не змінюється.
У разі успіху повертається вказівник на блок змінений у розмірі блок пам'яті. Цей вказівник завжди має тип void, і може бути приведений до необхідного типу даних при розіменовуванні. Якщо функції не вдалося змінити розмір блоку, повертається вказівник на NULL.
Приклад застосування функції calloc, програма №2:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *A,i;
if((A=(int*)malloc(10*sizeof(int)))==NULL)
exit(0);
for(i=0;i<10;++i)
A[i]=i;
if((A=(int*)realloc(A,33*sizeof(int)))==NULL)
exit(0);
for(;i<33;++i)
A[i]=i%3;
for(i=0;i<33;++i)
printf("%d ",A[i]);
free(A);
printf("\n");
system("PAUSE");
return 0;
}
Результат роботи програми:
Програма виділяє пам’ять для десяти елементів типу int та заповнює створений масив числами від 0 до 9. Після чого проводиться розширення масиву до розміру 33 елементів типу int. Доповнення масиву заповнюється остачами від ділення на 3 порядкового номеру комірки масиву, при чому вміст перших десяти елементів залишається незмінним.