Масиви

Масиви – це група елементів однакового типу (double, float, int і тощо). З оголошення масиву компілятор повинен одержати інформацію про тип елементів масиву і їх кількість. Оголошення масиву має два формати:

специфікатор-типу описувач [константний-вираз];

специфікатор-типу описувач [ ];

Описувач – це ідентифікатор масиву.

Специфікатор-типу задає тип елементів оголошуваного масиву. Елементами масиву не можуть бути функції і елементи типу void.

Константний вираз в квадратних дужках задає кількість елементів масиву. Константний вираз при оголошенні масиву може бути опущений в таких випадках:

  • при оголошенні масив ініціалізується;
  • масив оголошений як формальний параметр функції;
  • масив оголошений як посилання на масив, явно визначений в іншому файлі.

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

Кожне константних-виразів в квадратних дужках визначає число елементів по даному вимірюванню масиву, так що оголошення двомірного масиву містить два константних-вирази, тривимірного – три і т.д. Відзначимо, що в мові Сі перший елемент масиву має індекс рівний 0.

Приклади:

1int a[2][3];  /* представлене у вигляді матриці
2               а[0][0] а[0][1] а[0][2]
3               а[1][0] а[1][1] а[1][2]  */
4double b[10]; /* вектор з 10 елементів мають тип double */
5int w[3][3]= { { 2, 3, 4 },  { 3, 4, 8 }, { 1, 0, 9 } };

У останньому прикладі оголошений масив w[3][3]. Списки, виділені у фігурні дужки, відповідають рядкам масиву, у разі відсутності дужок ініціалізація буде виконана неправильно.

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

Приклади:

int s[2][3];

Якщо при зверненні до деякої функції написати s[0], то передаватиметься нульовий рядок масиву s.

int b[2][3][4];

При зверненні до масиву b можна написати, наприклад, b[1][2] і передаватиметься вектор з чотирьох елементів, а звертання b[1] дасть двомірний масив розміром 3 на 4. Не можна написати b[2][4], маючи на увазі, що передаватися буде вектор, тому що це не відповідає обмеженню накладеному на використання перетинів масиву.

Приклад оголошення символьного масиву.

char str[] = "оголошення символьного масиву";

Слід враховувати, що в символьному літералі знаходиться на один елемент більше, оскільки останній з елементів є управляючою послідовністю ‘\0’.

Структури

Структури – це складний об’єкт, в який входять елементи будь-яких типів, за винятком функцій. На відміну від масиву, який є однорідним об’єктом, структура може бути неоднорідною. Тип структури визначається записом виду:

struct { список визначень }

У структурі обов’язково повинен бути вказаний хоча б один компонент. Визначення структур має наступний вигляд:

тип-даних описувач;

де тип-даних вказує тип структури для об’єктів, визначених в описувачах. У простій формі описувачі є ідентифікаторами або масивами.

Приклад:

1struct { double x,y; } s1, s2, sm[9];
2struct { int  year; char moth, day; } date1, date2;

Змінні s1, s2 визначаються як структури, кожна з яких складається з двох компонент х і у. Змінна sm визначається як масив з дев’яти структур. Кожна з двох змінних date1, date2 складається з трьох компонентів year, moth, day.

Існує і інший спосіб асоціювання імені з типом структури, він заснований на використанні тега структури. Тег структури аналогічний тегу типу, що перераховує. Тег структури визначається таким чином:

struct тег { список описів; };

де тег є ідентифікатором.

У приведеному нижче прикладі ідентифікатор student описується як тег структури:

1struct student {
2   char name[25];
3   int id, age;
4   char prp;
5};

Тег структури використовується для подальшого оголошення структур даного вигляду у формі:

struct тег список-ідентифікаторів;

Приклад:

1struct studeut st1,st2;

Використання тегів структури необхідне для опису рекурсивних структур. Нижче розглядається використання рекурсивних тегів структури.

1struct node {
2   int data;
3   struct node *next;
4}  st1_node;

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

Доступ до компонентів структури здійснюється за допомогою вказівки імені структури і наступного через крапку імені виділеного компоненту, наприклад:

1st1.name="Іваненко";
2st2.id=st1.id;
3st1_node.data=st1.age;

Об’єднання (суміші)

Об’єднання подібно структурі, проте в кожен момент часу може використовуватися (або іншими словами бути у відповідь) тільки один з елементів об’єднання. Тип об’єднання може задаватися в наступному вигляді:

1union {
2   опис елементу 1;
3   ...
4   опис елементу n;
5};

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

Доступ до елементів об’єднання здійснюється тим же способом, що і до структур. Тег об’єднання може бути формалізований точно так, як і тег структури.

Об’єднання застосовується для наступних цілей:

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

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

Приклад:

 1union {
 2   char fullname[30];
 3   char address[80];
 4   int  age;
 5   int  telefon;
 6} inform;
 7
 8union {
 9   int ах;
10   char al[2];
11}  ua;

При використовуванні об’єкту infor типу union можна обробляти тільки той елемент який набув значення, тобто після присвоєння значення елементу inform.fullname, не має сенсу звертатися до інших елементів. Об’єднання ua дозволяє дістати окремий доступ до молодшого ua.al[0] і до старшого ua.al[1] байтам двобайтового числа ua.ax.

Поля бітів

Елементом структури може бути бітове поле, що забезпечує доступ до окремих біт пам’яті. Поза структурами бітові поля оголошувати не можна. Не можна також організовувати масиви бітових полів і не можна застосовувати до полів операцію визначення адреси. У загальному випадку тип структури з бітовим полем задається в наступному вигляді:

1struct {
2   unsigned ідентифікатор 1 : довжина-поля 1;
3   unsigned ідентифікатор 2 : довжина-поля 2;
4}

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

Приклад:

1struct {
2   unsigned a1 : 1;
3   unsigned a2 : 2;
4   unsigned a3 : 5;
5   unsigned a4 : 2;
6} prim;

Структури бітових полів можуть містити і знакові компоненти. Такі компоненти автоматично розміщуються на відповідних межах слів, при цьому деякі біти слів можуть залишатися невикористаними.

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

Змінні із змінною структурою

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

Розглянемо приклад, в якому інформація про геометричні фігури представляється на основі комбінованого використання структури і об’єднання.

1struct figure {
2   double area,perimetr;   /* загальні компоненти    */
3   int type;               /* ознака компоненту   */
4   union {                 /* перелік компонент */
5      double radius;       /* коло       */
6      double а[2];         /* прямокутник     */
7      double b[3];         /* трикутник      */
8   } geom_fig;
9} fig1, fig2 ;

У загальному випадку кожен об’єкт типу figure складатиметься з трьох компонентів: area, perimetr, type. Компонент type називається міткою активного компоненту, оскільки він використовується для вказівки, який з компонентів об’єднання geom_fig є активним в даний момент. Така структура називається змінною структурою, тому що її компоненти міняються залежно від значення мітки активного компоненту (значення type).

Відзначимо, що замість компоненти type типу int, доцільно було б використовувати перелічуваний тип. Наприклад, такий

1enum figure_geom {
2   CIRCLE,
3   BOX,
4   TRIANGLE
5};

Константи CIRCLE, BOX, TRIANGLE отримають значення відповідно рівні 0, 1, 2. Змінна type може бути оголошена перелічуваним типом: enum figure_geom type;

В цьому випадку компілятор СІ попередить програміста про потенційно помилкові присвоєння, таких, наприклад, як figure.type = 40;

У загальному випадку змінна структури складатиметься з трьох частин: набір загальних компонент, мітки активного компоненту і частини із змінними компонентами. Загальна форма змінної структури, має наступний вигляд:

 1struct {
 2   загальні компоненти;
 3   мітка активного компоненту;
 4   union {
 5      опис компоненти 1;
 6      опис компоненти 2;
 7      ...
 8      опис компоненти n;
 9   } ідентифікатор-об'єднання;
10} ідентифікатор-структури ;

Приклад визначення змінної структури з ім’ям health_record

 1struct {
 2      /* загальна інформація */
 3      char name [25];  /* ім'я    */
 4      int  age;     /* вік  */
 5      char sex;     /* стать    */
 6      /*  мітка  активного  компоненту */
 7      /*  (сімейний стан)      */
 8      enum martial_status ins;
 9      /* змінна частина */
10      union {
11         /* неодружений    */
12         /* немає компонент */
13         /* перебуває у шлюбі */
14         struct {
15            char marriage_date[8];
16            char spouse_name[25];
17            int no_children;
18         } marriage_info;
19         /* розлучений */
20         char date_divorced[8];
21      }  marital_info;
22} health_record;
23
24enum marital_status {
25   SINGLE, /* неодружений  */
26   MARRIED, /* одружений  */
27   DIVORCED /* розведений */
28} ;

Звертатися до компонентів структури можна за допомогою посилань:

health_record.name,

health_record.ins,

health_record.marriage_info.marriage_date.

Визначення об’єктів і типів

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

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

Відзначимо важливу особливість мови СІ, при оголошенні можна використовувати одночасно більше одного модифікатора, що дає можливість створювати безліч різних складних описувачів типів.

Проте треба пам’ятати, що деякі комбінації модифікаторів недопустимі:

  • елементами масивів не можуть бути функції,
  • функції не можуть повертати масиви або функції.

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

Для інтерпретації складних описів пропонується просте правило, яке звучить як “зсередини назовні”, і складається з чотирьох кроків.

Почати з ідентифікатора і подивитися управо, чи є квадратні або круглі дужки.

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

Приклади:

1   int * ( * comp [10]) ();
2    6  5   3  1     2    \4

У даному прикладі оголошується змінна comp (1), як масив з десяти (2) покажчиків (3) на функції (4), що повертають покажчики (5) на цілі значення (6).

1   char * ( * ( * var ) () ) [10];
2     7  6   4   2  1    3      5

Змінна var (1) оголошена як покажчик (2) на функцію (3) повертаючу покажчик (4) на масив (5) з 10 елементів, які є покажчиками (6) на значення типу char (7).

Окрім оголошень змінних різних типів, є можливість оголосити типи. Це можна зробити двома способами. Перший спосіб - вказати ім’я тега при оголошенні структури, об’єднання або переліку, а потім використовувати це ім’я в оголошенні змінних і функцій як посилання на цей тег. Другий - використовувати для оголошення типу ключове слово typedef.

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

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

Приклади:

 1   typedef double (* MATH)( );
 2   /* MATH - нове ім'я типу, представляюче покажчик на функцію, що повертає значення типу double      */
 3
 4   MATH cos;
 5
 6   /* cos покажчик на функцію, що повертає значення типу double */
 7
 8   /* Можна провести еквівалентне оголошення */
 9
10   double (* cos)();
11
12   typedef char FULLNAME[40];
13
14   /* FULLNAME - масив із сорока символів */
15
16   FULLNAME person;
17
18   /* Змінна person - масив із сорока символів */
19
20   /* Це еквівалентно оголошенню */
21
22   char person[40];

При оголошенні змінних і типів тут були використані імена типів (MATH FULLNAME). Крім цього, імена типів можуть ще використовуватися в трьох випадках: у списку формальних параметрів, в оголошенні функцій, в операціях приведення типів і в операції sizeof (операція приведення типу).

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

1   специфікатор-типу абстрактний визначник;

Абстрактний визначник – це визначник без ідентифікатора, що складається з одного або більш модифікаторів покажчика, масиву або функції. Модифікатор покажчика * завжди задається перед ідентифікатором в визначнику, а модифікатори масиву [] і функції () - після нього. Так, щоб правильно інтерпретувати абстрактний визначник, потрібно почати інтерпретацію з ідентифікатора, що мається на увазі.

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

Ініціалізація даних

При оголошенні змінної їй можна привласнити початкове значення, приєднуючи ініціатор до описувача. Ініціатор починається із знаку “=” і має наступні форми.

1Формат 1: = ініціатор;
2
3Формат 2: = { список - ініціаторів };

Формат 1 використовується при ініціалізації змінних основних типів і покажчиків, а формат 2 - при ініціалізації складових об’єктів.

Приклади:

1char tol = 'N';

Змінна tol ініціалізується символом ‘N’.

1const long megabute = (1024 * 1024);

Змінна megabute, що не модифікується, ініціалізується константним виразом після чого вона не може бути змінена.

1static int b[2][2]= {1,2,3,4};

Ініціалізується двомірний масив b цілих величин елементам масиву привласнюються значення із списку. Ця ж ініціалізація може бути виконана таким чином:

1static int b[2][2]= { { 1,2 }, { 3,4 } };

При ініціалізації масиву можна опустити одну або декілька розмірностей

1static int b[2][] = { { 1,2 }, { 3,4 } };

Якщо при ініціалізації вказано менше значень для рядків, то елементи, що залишилися, ініціалізуються 0, тобто при описі

1static int b[2][2]= { { 1,2 }, { 3 } };

елементи першого рядка набудуть значення 1 і 2, а другий 3 і 0.

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

Приклади:

1struct complex
2{
3   double real;
4   double imag;
5} comp [2][3] = { { {1,1}, {2, 3}, {4, 5} },
6                  { {6,7}, {8,9}, {10,11} } };

У даному прикладі ініціалізується масив структур comp з двох рядків і трьох стовпців, де кожна структура складається з двох елементів real і imag.

1struct complex comp2 [2][3] =
2   { {1,1},{2,3},{4,5},
3     {6,7},{8,9},{10,11}};

В даному прикладі компілятор інтерпретує дані фігурні дужки таким чином:

  • перша ліва фігурна дужка - початок складового ініціатора для масиву comp2;
  • друга ліва фігурна дужка - початок ініціалізації першого рядка масиву comp2[0]. Значення 1,1 привласнюються двом елементам першої структури;
  • перша права дужка (після 1) вказує компілятору, що список ініціаторів для рядка масиву закінчений, і елементи структур, що залишилися, в рядку comp[0] автоматично ініціалізуються нулем;
  • аналогічно список {2,3} ініціалізує першу структуру в рядку comp[1], а структури масиву, що залишилися, звертаються в нулі;
  • на наступний список ініціалізацій {4,5} компілятор повідомлятиме про можливу помилку оскільки рядок 3 в масиві comp2 відсутній.

При ініціалізації об’єднання задається значення першого елементу об’єднання відповідно до його типу.

Приклад:

1union tab {
2   unsigned char name[10];
3   int tab1;
4} pers = {'A','H','T','O','H'};

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

Ініціалізацію масиву символів можна виконати шляхом використання рядкового літерала.

1char greetings[ ]= "привіт";

Ініціалізується масив символів з 7 елементів, останнім елементом (сьомим) буде символ ‘\0’, яким завершуються всі рядкові літерали.

В тому випадку, якщо задається розмір масиву, а рядковий літерал довший, ніж розмір масиву, то зайві символи відкидаються.

Наступне оголошення ініціалізує змінну line як масив, що складається з семи елементів.

1  char line[5]= "привіт";

У змінну line потрапляють перші п’ять елементів літерала, а символи 'Т' і '\0' відкидаються.

Якщо рядок коротший, ніж розмір масиву, то елементи масиву, що залишилися, заповнюються нулями.

Відзначимо, що ініціалізація змінної типу tab може мати наступний вигляд:

***union tab pers1 = “Антон”;

і, таким чином, в символьний масив потраплять символи:

1'А','Н','Т','О','Н','\0'

а решта елементів ініціалізує нулем.