Деякі особливості типів даних C

Базові типи даних

Як зазначалося раніше, у мові C є кілька базових типів даних: char (символьний), int (цілий), float (із плаваючою точкою), double (із плаваючою точкою подвійної точності).

Кількість бітів, що відводяться під об'єкти, залежить від конкретної машини. Крім того, існують кваліфікатори, які можна використовувати: short (коротке), long (довге) та unsigned (без знака). Наприклад:

short int x;long int y;unsigned int z;

Інформацію про базові числові типи даних із кваліфікаторами подано в табл. 4.1.

Таблиця 4.1

Повна назва типу Скорочена назва типу Розмір у байтах
Signed char Char  
Signed int Signed,int ~
Signed short int Short, Signed short  
Signed long int Long, Signed long  
Unsigned char Unsigned char  
Unsigned short int Unsigned short  
Unsigned long int Unsigned long  
Unsigned int Unsigned ~
Float    
Double    
Long double    
~-розмір залежить від типу комп'ютера    

Існують ще деякі специфічні типи:

a void – відсутність типу даних;

a enum – перераховний тип даних.

Синтаксично тип enum задається так:

enum[<тег>]{<список переліку>}[<описувач>][<описувач>…]

Список переліку задає послідовність іменованих констант, яким у випадку відсутності ініціалізатора присвоюється значення за умовчанням 0, 1, 2,... Наприклад:

enum numbers{zero,one,two} a;

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

enum numbers {zero,one,two=5,three,four} a;

Тут zero==0, three==6, four==7.

Перетворення типів

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

char c;

int i;

c=i;

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

При присвоюванні значення змінної плаваючого типу змінній цілого воно перетворюється на значення змінної типу int, а потім – типу операнда, що стоїть у лівій частині операції присвоювання.

Розглянемо фрагмент програми, що переводить послідовність цифр у числовий еквівалент:

int atoi(char s[]) /*char* s*/

{int i,n=0;

for (i=0;s[i]>'0' && s[i]<'9';i++)

n=n*10+(s[i]-'0');

return n;

}

Тут у рядку n=n*10+(s[i]-'0'); у результаті віднімання s[i]-'0' отримуємо числове значення (цифру), що задається символом s[i]. Дійсно, s[i] містить код відповідної цифри. Щоб отримати числове значення, необхідно врахувати, що цифри розміщуються в системі кодування підряд, цифра '0' як символ також має деякий код (для нас не важливо, який саме). Множення на 10 у циклі дозволяє постійно зсувати цифрове зображення ліворуч, залишаючи вільним молодший розряд для нової цифри. Розглянемо роботу програми.

Нехай на вхід функції подається рядок "3246". Результати роботи програми подано у табл. 4.2.

Таблиця 4.2

№ ітерації (i) Значення s[i] Значення n у циклі
  '3'  
  '2'  
  '4'  
  '6'  

Засіб typedef

Мова С має спеціальний механізм, який дозволяє давати певним конструкціям для визначення типу нові імена типів:

typedef<специфікатор типу><описувач>[<описувач>,..]

typedef int*pint;

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

typedef (*(*** newtype) [10]) (char*,int*)

newtype a,b,c;

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

typedef struct students *treeptr;

Struct student

{char name [20];

int curs;

treeptr left,right;}

Засіб typedef аналогічний макровизначенню #define. Однак існують ситуації, коли через директиву #define неможливо визначити конструкцію, яка визначається через директиву typedef. Наприклад:

typedef int*(*pfun)(char*,char*);

pfun a; //це аналогічно int *(*a)(char*,char*);

typedef використовується:

a для визначення читабельніших типових конструкцій;

a для скорочення оголошень;

a для визначення типів даних, що не залежать від реалізації операційної системи й типу комп'ютера;

a з естетичних міркувань.

Покажчики та масиви

Покажчики. Покажчик – це змінна, яка зберігає адресу об'єкта деякого типу. Покажчик на функцію містить адресу точки входу у функцію. Синтаксично визначення покажчика задається як [<тип>]*<описувач>. Наприклад:

int * pi, i=12;

pi=&i;

<тип> може задавати базовий тип, перераховний, структурний та об'єднання.

Особливе місце займають покажчики на тип void. Покажчик на void може вказувати на значення будь-якого типу (містити адресу елемента будь-якого типу). Наприклад:

void * p;

int i;

float f;

p=&i;

p=&f;

Однак отримати значення покажчика на тип void, використовуючи його в арифметичному виразі, не можна. У виразах *v чи v++ буде помилка. Для того, щоб покажчик на тип void можна було використовувати, необхідно застосовувати операцію явного зведення типу. Наприклад:

(тип*) <ідентифікатор>:

j=(int*)v;

*((int*)v)++;

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

Змінна, оголошена як покажчик, зберігає адресу пам'яті. Однак покажчики на один i той самий тип даних не обов'язково мають однаковий розмір і формат.

Дії над покажчиками:

1) можна порівнювати їх з нулем i присвоювати їм значення 0;

2) якщо р i q – покажчики на елементи одного масиву, то їх можна порівнювати;

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

Якщо описано int * p;, то p+4 міститиме адресу елемента типу int, розміщеного на відстані чотирьох таких елементів від p:

4) від покажчика можна віднімати ціле число;

5) допускається віднімання показчиків p i q – у результаті отримуємо кількість елементів між p i q.

Масиви. Синтаксично масив задається як

[<тип>]<описувач>[ ]

або

[<тип>]<описувач>[константний вираз]

Наприклад:

int array[12];

Оголошення масиву визначає тип його елементів та ім'я. Воно може також визначати кількість елементів у масиві. Якщо специфікація типу опущена, то масив є цілим. Ім'я масиву має специфічний контекст – воно містить адресу першого елемента масиву.

Описувач не може бути функцією чи мати тип void. Не допускається масив типу void. Індексація масивів починається з 0.

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

int a[]={1,2,3}

void f(int a[])

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

[<тип>]<описувач>[<конст. вираз1>][<конст. вираз2>]...

Кожний елемент константного виразу визначає кількість елементів у даному вимiрi.

Елементи багатовимірних масивів розміщуються по рядках.

Ініціалізація масивів здійснюється так:

int a[ ]={1,2,3};

int a[3][2]={{1,2},{3,4},{6,7}};

Причому присутність внутрішніх дужок необов'язкова.

Індексний вираз. У мові С існує поняття індексного виразу, який задається за допомогою квадратних дужок. Його значення обчислюється за такою схемою: до покажчика додається константний вираз у дужках, а потім береться значення утвореного покажчика. Якщо описати, наприклад, покажчик int *p; то можна використовувати індексний вираз p[const], еквівалентний конструкції *(p+const), де const – деякий константний вираз. Аналогічно, якщо описано масив int a[5]; то можна використовувати конструкцію вигляду *(а+3), еквівалентну а[3].

За синтаксичною схемою індексного виразу допускаються й від'єм­ні індекси масивів: a[-3]. Якщо описати масиви

int a[3]={1,3,5};

int b[4]={2,4,6,8};

то b[-1]==5, b[-2]==3, b[-3]==1.

Механізм обчислення індексного виразу допускає можливість зміни порядку наступності константного виразу та покажчика (чи імені масиву). Тому запис a[4] еквівалентний 4[a] (адже значення виразів *(a+4) та *(4+a) однакові).

Якщо ім'я масиву містить адресу першого елемента масиву, то можемо описати покажчик, ініціалізувавши його іменем масиву:

int a[4],*pa;

pa=a;

Тоді pa[0]==a[0], pa[1]==a[1] і т. д.

Можемо присвоїти покажчику pa та адресу будь-якого іншого елемента масиву а: pa=&a[2]; Тоді pa[0]==a[2], pa[1]==a[3] і т. д.

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

int f(char * s)

{int i;

for (i=0;*s='\0';i++;s++);

return i;}

/*функція обраховує кількість елементів рядка*/

char s[10];

Можемо викликати функцію

f(&s[i]); або f(s+i).

Допускається існування масиву покажчикiв:

int * a[20];

Покажчики на масив і багатовимірні масиви. Визначення int(*a)[20] задає покажчик на масив. У цьому випадку ім'я а також задає адресу першого елемента масиву, але додавання до такого покажчика, наприклад, одиниці приведе до збільшення адреси на всю довжину масиву – у даному випадку – 20 елементів типу int.

Для розуміння механізму інтерпретації багатовимірних масивів можемо навести такі міркування. Одновимірний масив int a[10] може бути розглянутий через покажчик (а містить адресу першого елемента):

int * pa;

pa=a;

pa++==a[1];

Двовимірний масив int a[5][10] інтерпретується як покажчик на масив із 10 елементів:

int (*pa)[10];

pa=a;

Тоді

*(pa++)==a[1][0];

*(*(pa)+1)==a[1][1];

Аналогічно, якщо розглянути, наприклад, тривимірний масив

int b[10][20][30];

то він інтерпретується як покажчик на двовимірний масив розмірністю 20 ´ 30:

int (*pb)[20][30];

pb=b;

Тоді ***(pb++)==b[1][0][0], *(**(pb++)+1)==b[1][0][1].

Ім'я b міститиме адресу тривимірного масиву (першого його елемента). Тоді b[0],b[1],…,b[9] – адреси відповідних двовимірних масивів, b[i][j] – одновимірних. Наприклад:

int d[1][3][4]={1,2,3,4,5,6,7,8,9,10,11,12};

int d1[1][3][4]={111,2,3,4,5,6,7,8,9,10,11,12};

Main()

{

printf("%d',***(d+1));

}

Буде надруковане значення 111 – першого елемента масиву d1.

У загальному випадку доступ до елемента n -вимірного масиву a[N][N]…[N] здійснюється за таким правилом:

a[m1][m2]…[mn]=*(a[0][0]…[0]+m1*Nn-1 +m2*Nn-2+…+mn)

n-1 нульових індексів

При роботі з динамічними масивами необхідно звернути особливу увагу на виділення пам'яті за допомогою функцій malloc чи calloc.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: