МІНІСТЕРСТВО ОСВІТИ І НАУКИ УКРАЇНИ
НАЦІОНАЛЬНИЙ УНІВЕРСИТЕТ “ЛЬВІВСЬКА ПОЛІТЕХНІКА”
Кафедра ЕОМ
"Внутрішнє представлення в пам’яті комп’ютера статичних даних похідних типів"
МЕТОДИЧНІ ВКАЗІВКИ
до лабораторної роботи № 1
з дисципліни
" Програмування. Частина III.
Структури даних та алгоритми "
для студентів напряму
6.050102 “Комп’ютерна інженерія”
Львів – 2009
Методичні вказівки до лабораторної роботи "Внутрішнє представлення в пам’яті комп’ютера статичних даних похідних типів" з дисципліни “Програмування. Частина IIІ. Структури даних та алгоритми" для підготовки студентів напряму 6.050102 “Комп’ютерна інженерія” / Укл. Т.А.Лисак – Львів: Видавництво НУ “Львівська політехніка”, 2009 – 27 с.
Укладач: Лисак Т.А., ст. викладач каф.ЕОМ
Відповідальний
за випуск: Мельник А.О., д-р техн. наук, проф.
Рецензенти: Мороз І.В., ст. викладач каф.ЕОМ
Юрчак І.Ю., доцент кафедри САПР, к.т.н.
1. МЕТА РОБОТИ
Дослідження внутрішнього представлення в пам’яті комп’ютера похідних типів даних статичної структури.
2. ТЕОРЕТИЧНІ ВІДОМОСТІ
2.1. Похідні типи даних
Переліки
Змінна, котра може приймати значення з деякого списку значень, називається змінною перелікового типу або переліком. Оголошення переліку задає тип змінної переліку і визначає список іменованих констант, що називається списком переліку. Імена елементів списку переліку задаються в фігурних дужках через кому.
Значенням кожного імені списку є деяке ціле число. Змінна типу переліку може приймати значення однієї з іменованих констант списку. Змінні типу enum можуть використовуватись і як операнды в арифметичних операціях та в операціях відношення, і як індекси в індексних виразах.
Список переліку може містити одну або декілька конструкцій виду:
ідентифікатор [= константний вираз]
Кожен ідентифікатор іменує елемент переліку. Всі ідентифікатори в списку переліку повинні бути унікальними і повинні відрізнятись від всіх інших ідентифікаторів у тій самій області видимості, включаючи імена звичайних змінних і ідентифікатори з інших списків переліку.
У випадку відсутності константного виразу перший ідентифікатор набуває значення 0, наступний ідентифікатор - значення 1 і т.д. Отже, пам'ять, що відводиться під змінну типу перелік - це пам'ять, необхідна для розміщення значення типу іnt.
Ідентифікатор, зв'язаний з константним виразом, приймає значення, що задається цим константним виразом. Константний вираз повинен мати тип іnt і може бути як додатнім, так і від’ємним. Константні вирази можуть містити однакові значення. Наступний ідентифікатор в списку отримує значення, рівне константному виразу плюс 1, якщо цей ідентифікатор не має свого константного виразу.
Приклад 1.
enum week {
SUBOTA ,
NEDILJA = 0,
PONEDILOK,
VIVTOROK,
SEREDA = VIVTOROK * 2,
CHETVER,
PJATNYTCJA
};
week den = SEREDA;
У цьому прикладі визначається тип перелік з іменем week і оголошується змінна den цього типу переліку. З ідентифікатором SUBOTA за замовчуванням асоціюється значення 0. Ідентифікатор NEDILJA явно встановлюється в 0. Ідентифікатори PONEDILOK та VIVTOROK за замовчуванням набувають значень 1 та 2 відповідно. Константний вираз VIVTOROK * 2 дорівнює значенню 4 і це значення надається ідентифікаторові SEREDA. Ідентифікатори, що залишилися, за замовчуванням приймають значення 5 та 6 відповідно.
В пам’яті комп’ютера під зміну den буде виділено 1 байт пам’яті в якому буде записана наступна послідовність: 0000 0100.
Вказівники
Коли компілятор обробляє оператор визначення змінної, наприклад, іnt і=10, він виділяє пам'ять відповідно до типу (в прикладі іnt) та ініціалізує її заданим значенням (в прикладі 10). Всі звертання в програмі до змінного по її імені (в прикладі і) заміняються компілятором на адресу області пам'яті, в якій зберігається значення змінної. Можна також визначити власні змінні для збереження адрес областей пам'яті. Такі змінні називаються вказівниками.
У мові C++ розрізняють три види вказівників: вказівники на функцію, на об'єкт і на voіd, що відрізняються властивостями і набором допустимих операцій. Вказівник не є самостійним типом, він завжди зв'язаний з яким-небудь іншим конкретним типом.
Вказівник на функцію містить адресу в сегменті коду, по якій розташовується код функції, тобто адресу, по якій передається керування при виклику функції. Вказівники на функції використовуються для непрямого виклику функції (не через її ім'я, а через звертання до змінної, що зберігає її адресу), а також для передачі імені функції в іншу функцію як параметра.
Вказівник на функцю має тип "вказівник функції, що повертає значення заданого типу і має аргументи заданого типу":
тип (*ім'я) ( список_типів_аргументів );
Наприклад, оголошення:
іnt (*fun) (double, double);
задає вказівник з ім'ям fun на функцію, що повертає значення типу іnt і має два аргументи типу double.
Вказівник на об'єкт містить адресу області пам'яті, у якій зберігаються дані визначеного типу (основного або складеного). Найпростіше оголошення вказівника на об'єкт (далі будемо називати просто вказівник) має вигляд:
тип *ім'я;
де тип може бути довільним, крім посилання і бітового поля, причому тип може бути до цього моменту тільки оголошений, але ще не визначений (наприклад, в структурі може бути присутнім вказівник на структуру того ж типу). Можна визначити вказівник на вказівник.
Зірочка відноситься безпосередньо до імені, тому для того, щоб оголосити кілька вказівників, потрібно ставити її перед ім'ям кожного з них. Наприклад, в операторі
іnt *a, b, *c;
описуються два вказівники на ціле з іменами а і с, а також ціла змінна b.
Розмір вказівника залежить від моделі пам'яті. Значенням вказівника є адреса оперативної пам’яті, яка складається з адреси сегмента (номера сегмента оперативної пам’яті) і зміщення (адреси стосовно початку сегмента). Формат адреси:
<сегмент> : <зміщення>
Адреса сегмента зберігається в старшому слові, а зміщення – у молодшому слові повної адреси.
Вказівник на voіd застосовується в тих випадках, коли конкретний тип об'єкта, адресу якого потрібно зберігати, не визначений (наприклад, якщо в одній і тій самій змінній в різні моменти часу потрібно зберігати адреси об'єктів різних типів).
Вказівникові на voіd можна присвоїти значення вказівника будь-якого типу, а також порівнювати його з будь-якими вказівниками, але перед виконанням будь-яких дій з областю пам'яті, на яку він посилається, потрібно явно перетворити його до конкретного типу. Вказівник може бути константою або змінною, а також вказувати на константу або змінну.
Наприклад:
unsіgned іnt *a; // змінна а – це вказівник на тип unsіgned іnt
double *x; // вказівник х вказує на тип double
char *fuffer ; // змінна fuffer – це вказівник на тип char
double nomer;
voіd *addres;
addres = & nomer;
(double *) addres ++;
/* Змінна addres оголошується як вказівник на об'єкт будь-якого типу. Тому їй можна присвоїти адресу будь-якого об'єкта (& - операція взяття адреси). Однак, як зазначено вище, жодна арифметична операція не може бути виконана над вказівником, поки не буде явно визначено тип даних, на які він вказує. Це можна зробити, використовуючи операцію приведення типу (double *) для перетворення змінної addres до вказівника на тип double, а потім постфіксне збільшення значення (++) цієї адреси. */
const *d; // змінна d оголошується як вказівник на константний вираз, тобто
// значення вказівника може змінюватися в процесі виконання
// програми, але величина, на яку він вказує, не може змінюватись
char *const v = &obj; // змінна v оголошується як константний вказівник на дані типу
// char, тобто на протязі всієї програми w буде вказувати на ту
// саму область пам'яті, але зміст цієї області може бути змінений
Як видно з прикладів, модифікатор const, що знаходиться між ім'ям вказівника і зірочкою, відноситься до самого вказівника і забороняє його зміну, а const ліворуч від зірочки задає сталість значення, на яке він вказує.
Величини типу вказівник підкоряються загальним правилам визначення області дії, видимості і часу життя.
Вказівники найчастіше використовують при роботі з динамічною пам'яттю, що ще називається купою (переклад з англійської мови слова heap). Це вільна пам'ять, у якій можна під час виконання програми виділяти місце відповідно до потреб. Доступ до виділених ділянок динамічної пам'яті, що називаються динамічними змінними, виконується тільки через вказівники. Час життя динамічних змінних - від початку створення до кінця програми або до явного звільнення пам'яті. В мові C++ використовується два способи роботи з динамічною пам'яттю. Перший використовує сімейство функцій malloc (дісталося в спадщину від мови С), другий використовує операції new і delete.
При визначенні вказівника треба прагнути виконати його ініціалізацію, тобто присвоєння початкового значення. Ненавмисне використання неініціалізованих вказівників - розповсюджене джерело помилок у програмах. Ініціалізатор записується зразу після імені вказівника або в круглих дужках, або після знака рівності.
Існують такі способи ініціалізації вказівника:
1. Присвоювання вказівникові адреси існуючого об'єкта:
за допомогою операції взяття адреси:
іnt а = 5; // ціла змінна
іnt *р = &а; // у вказівник р записується адреса змінної а
іnt *р (&а): // те саме іншим способом
за допомогою значення іншого ініціалізованого вказівника:
іnt *r = р;
за допомогою імені масиву, що трактується як адреса:
іnt b[10]; // масив
іnt *t = b; // у вказівник t записується адреса початку масиву
за допомогою імені функції, що трактується як адреса:
voіd f (іnt а ){ /* ... * / } // визначення функції
voіd (*pf) (іnt); // вказівник на функцію
pf = f; // присвоювання адреси функції
2. Присвоювання вказівникові адреси області пам'яті в явному вигляді:
char *vp = (char *) 0хB8000000; // 0хB8000000- шестнадцатеричная константа,
// (char *) - операція приведення типу: константа
// приводиться до типу "вказівник на char"
3. Присвоювання порожнього значення:
іnt *s = NULL;
іnt *rl = 0:
В першому рядку використовується константа NULL. Рекомендується використовувати просто число 0, тому що це значення типу іnt правильно перетворюється стандартними способами відповідно до контексту. Оскільки гарантується, що об'єктів з нульовою адресою немає, порожній вказівник можна використовувати для перевірки, чи посилається вказівник на деякий конкретний об'єкт чи ні.
4. Виділення ділянки динамічної пам'яті і присвоєння її адреси вказівникові:
за допомогою операції new:
іnt *n = new іnt; // 1
іnt *k = new іnt (10); // 2
іnt *q = new іnt [10]; // 3
за допомогою функції malloc:
іnt *u = (іnt *) malloc(sizeof(іnt)); // 4
В операторі 1 операція new виконує виділення ділянки динамічної пам'яті необхідної для розміщення величини типу іnt і записує адресу початку цієї ділянки в змінну n. Пам'ять під саму змінну n (тобто пам'ять для розміщення вказівника) виділяється на етапі компіляції.
В операторі 2, крім описаних вище дій, виконується ініціалізація виділеної динамічної пам'яті значенням 10.
В операторі 3 операція new виконує виділення динамічної пам'яті під 10 величин типу іnt (масиву з 10 елементів) і записує адресу початку цієї ділянки в змінну q, яка може трактуватись як ім'я масиву. Через ім'я можна звертатись до будь-якого елемента масиву.
Якщо пам'ять виділити не вдалося, то по стандарту повинне породжуватись виключення bad_alloc. Старі версії компіляторів можуть повертати значення 0.
В операторі 4 робиться те саме, що й в операторі 1, але за допомогою функції виділення пам'яті malloc. Для того щоб використовувати malloc, потрібно підключити до програми заголовний файл <malloc.h>. В функцію передається один параметр - кількість виділюваної пам'яті в байтах. Конструкція (іnt*) використовується для приведення типу вказівника, що повертається функцією, до необхідного типу. Якщо пам'ять виділити не вдалося, функція повертає значення 0.
Бажано переважно використовувати операцію new, ніж функцію malloc, особливо при роботі з об'єктами.
Звільнення пам'яті, виділеної за допомогою операції new, виконується за допомогою операції delete, а пам'яті, виділеної функцією malloc - за допомогою функції free. При цьому змінна-вказівник зберігається і може бути ініціалізована повторно. Наведені вище динамічні змінні знищуються в такий спосіб:
delete n; delete k; delete [ ] q; free (u);
Якщо пам'ять виділялася за допомогою new[], для звільнення пам'яті необхідно застосовувати delete[]. Розмірність масиву при цьому не вказується. Якщо квадратних дужок немає, то ніякого повідомлення про помилку не видається, але помічений як вільний буде лише перший елемент масиву, а інші виявляться недоступними для подальших операцій. Такі комірки пам'яті називаються сміттям.
Наголосимо ще раз, що якщо змінна-вказівник виходить з області своєї дії, відведена під неї пам'ять звільняється. Отже, динамічна змінна, на яку посилався вказівник, стає недоступною. При цьому пам'ять з-під самої динамічної змінної не звільняється. Інший випадок появи сміття - коли ініціалізованному вказівникові присвоюється значення іншого вказівника. При цьому старе значення безвісти губиться.
За допомогою комбінацій зірочок, круглих і квадратних дужок можна описувати складені типи і вказівники на складені типи, наприклад, в операторі
іnt *(*р[10])();
оголошується масив з 10 вказівників на функції без параметрів, що повертають вказівники на іnt.
За замовчуванням квадратні і круглі дужки мають однаковий пріоритет, більший, ніж зірочка, і розглядаються зліва направо. Для зміни порядку розгляду використовуються круглі дужки.
При інтерпретації складних описів необхідно дотримуватись правила "зсередини назовні" , суть якого полягає в наступному:
1) якщо справа від імені стоять квадратні дужки, то це масив, якщо дужки круглі, то це функція;
2) якщо зліва є зірочка, то це вказівник на проінтерпретовану раніше конструкцію;
3) якщо справа зустрічається закриваюча кругла дужка, то необхідно застосувати наведені вище правила всередині дужок, а потім переходити назовні;
4) в останню чергу інтерпретується специфікатор типу.
Для приведеного вище опису порядок інтерпретації зазначений цифрами:
іnt * ( * р [10] ) ( ) ;
// 5 4 2 1 3 - порядок інтерпретації опису
З вказівниками можна виконувати такі операції:
розадресація, або непряме звертання до об'єкта (*),
присвоювання,
додавання з константою,
віднімання,
інкремент (+ +),
декремент (– –),
порівняння,
приведення типів.
При роботі з вказівниками часто використовується операція взяття адреси (&).
Операція розадресації, або розіменувания, призначена для доступу до величини, адреса якої зберігається в вказівнику. Цю операцію можна використовувати як для одержання, так і для зміни значення величини (якщо вона не оголошена як константа):
char а = 'd'; // змінна типу char
char *р = new char; // виділення пам'яті під вказівник і під динамічну змінну типу char
*р = 'д'; а = *р; // присвоювання значень обом змінним
Присвоювання вказівників на об'єкти вказівникам на функції (і навпаки) неприпустимо. Заборонено і присвоювання значення вказівникам-константам, втім, як і константам будь-якого типу (однак, допускається присвоювати значення вказівнику на константу і змінній, на яку посилається вказівник-константа).
Арифметичні операції з вказівниками (додавання з константою, віднімання, інкремент і декремент) автоматично враховують розмір типу величини, на яку посилається вказівник. Ці операції можуть бути застусовані тільки до вказівників одного типу і в основному при роботі зі структурами даних, послідовно розміщеними в пам'яті, наприклад, з масивами.
Інкремент переміщює вказівник до наступного елемента масиву, декремент - до попереднього. Фактично значення вказівника змінюється на величину sіzeof (тип). Якщо вказівник на визначений тип збільшується або зменшується на константу, його значення змінюється на величину цієї константи, помножену на розмір об'єкта даного типу, наприклад:
short *р = new short [5];
р++; // значення р збільшується на 2 байта
long *q = new long [5];
q++; // значення q збільшується на 4 байта
Різниця двох вказівників - це різниця їх значень, поділена на розмір типу в байтах (наприклад для масивів різниця вказівників на третій і шостий елементи дорівнює 3). Додавання двох вказівників не допускається. При записі виразів з вказівниками треба звертати увагу на пріоритети операцій. Як приклад розглянемо послідовність дій, задану в операторі
*р++ = 10;
Операції розадресації та інкремента мають однаковий пріоритет і виконуються справа наліво, але, оскільки інкремент постфіксний, він виконується після виконання операції присвоювання. Таким чином, спочатку за адресою, що міститься в вказівнику р, буде записане значення 10, а потім вказівник буде збільшений на кількість байт, що відповідає його типові. Те ж саме можна записати докладніше: *р = 10: р++;
Вираз (*р)++ , навпаки, інкрементує значення, на яке посилається вказівник.
Посилання
Посилання являє собою синонім імені, зазначеного при ініціалізації посилання. Посилання можна розглядати як вказівник, що завжди разіменовується. Формат оголошення посилання:
тип & ім'я;
де тип - це тип величини, на яку вказує посилання, & - оператор посилання, що означає, що наступне за ним ім'я є ім'ям змінної типу посилання, наприклад:
іnt kol;
int & pal = kol; // посилання pal - альтернативне ім'я для kol
const char & CR = ' \ n ' : // посилання на константу
Для посилання діють наступні правила:
Змінна-посилання повинна явно ініціалізуватися при її описі, крім випадків, коли вона є параметром функції, описана як extern або посилається на поле даних класу.
Після ініціалізації посиланню не може бути присвоєна інша змінна.
Тип посилання повинен збігатися з типом величини, на яку воно посилається.
Не дозволяється визначати вказівники на посилання, створювати масиви посилань і посилання на посилання.
Посилання застосовуються найчастіше як параметри функцій і типів значень, що повертаються функціями. Посилання дозволяють використовувати у функціях змінні, що передаються за адресою, без операції розадресації, що поліпшує читабельність програми.
Посилання, на відміну від вказівника, не займає додаткового простору в пам'яті і є просто іншим ім'ям величини. Операція над посиланням приводить до зміни величини, на яку вона посилається.
Масиви
Масив - це впорядкований скінченний набір даних одного типу, які зберігаються в послідовно розташованих комірках оперативної пам'яті і мають спільну назву. З оголошення масиву компілятор одержує інформацію про тип елементів масиву та їх кількість.
Для роботи з масивом його елементи індексуються (нумеруються), а доступ до них здійснюється за допомогою операції взяття індексу. В мові С++ індексація масивів починається з 0, тому елемент із індексом 1 насправді є другим елементом масиву, а індекс першого дорівнює 0. Індекс може бути цілим числом або цілим виразом. Якщо в якості індекса використовується вираз, то спочатку обчислюється вираз, щоб визначити конкретний елемент масиву з яким буде виконуватись робота.
Елементам масиву можна задати початкові значення (ініціалізувати їх) в оголошенні масиву за допомогою списку, що знаходиться одразу після оголошення. Список містить однакові за змістом початкові значення, розділені комами і обмежені фігурними дужками, наприклад:
int b[3] = {3, 2, 1}; // аналогічно присвоєнням : b[0]=3; b[l]=2; b[2]=l;
Якщо початкових значень меньше, ніж елементів в масиві, то елементам, що залишились автоматично надаються нульові початкові значення. Наприклад, елементам масиву b можна присвоїти нульові початкові значення за допомогою оголошення
int b[12] = {3, 2, 1};
яке явно надає початкові значення першим трьом елементам масиву і неявно надає нульові початкові значення решті дев’яти елементам, оскільки початкових значень меньше, ніж оголошено елементів масиву.
Однак, по замовчуванню автоматично масив не отримує нульові початкові значення неявно. Треба присвоїти нульові початкові значення хоча б першому елементу для того, щоб автоматично були обнулені всі решта елементів, наприклад:
int b[5] = {0}; // аналогічно присвоєнням : b[0]=0; b[l]=0; b[2]=0; b[3]=0; b[4]=0;
Синтаксичною помилкою буде задання в списку більшої кількості початкових значень, ніж є елементів в масиві. Наприклад, оголошення масиву
int b[5] = {5, 4, 3, 2, 1, 0};
призведе до синтаксичної помилки, оскільки список ініціалізації містить 6 початкових значень, а масив має тільки 5 елементів.
Використання ідентифікатора масива в програмі еквівалентно використанню адреси його першого елемента. Прохід по масиву можна здійснювати за допомогою індексів або за допомогою вказівників.
Незважаючи на те, що в мові С++ вбудована підтримка для типу даних "масив", вона досить обмежена. Фактично є можливість доступу тільки до окремих елементів масиву. Мова С++ не підтримує абстракцію масиву, не існує операцій над масивами в цілому, таких, наприклад, як присвоєння значень одного масива іншому або порівняння двох масивів на рівність. Замість цього потрібно програмувати такі операції за допомогою циклів поелементно.
В мові С++ також використовуються багатовимірні масиви, при оголошенні яких необхідно вказувати праву границю кожного виміру в окремих квадратних дужках. При звертанні до елементів багатовимірного масиву необхідно вказувати індекси для кожного виміру.
Багатовимірні масиви можуть отримувати початкові значення в своїх оголошеннях так само, як і одновимірні масиви. При ініціалізації багатомірного масиву він представляється як масив масивів, при цьому кожен масив знаходиться у своїх фігурних дужках (у цьому випадку ліву розмірність при описі можна не вказувати). Наприклад, двовимірний масив b[2][2] можна оголосити і надати йому початкові значення наступним чином:
int b[][2]={{1, 2},{3, 4}}; //аналогічно присвоєнням: b[0][0]=1; b[0][l]=2; b[1][0]=3; b[1][1]=4;
Якщо початкових значень в деякому підрядку не вистачає для їх присвоєння всім елементам рядка масиву, то всім решта елементам рядка, що залишились, присвоюються нульові початкові значення. Наприклад, оголошення
int b[2][2] = {{1, } , {3, 4}};
буде означати, що b[0][0] отримує початкове значення 1, b[0][1] отримує початкове значення 0, b[1][0] отримує початкове значення 3 і b[1][1] отримує початкове значення 4.
Якщо зі списку початкових значень забрати всі фігурні дужки навколо кожного підрядка, то компілятор автоматичнонадасть перші початкові значення елементам першого рядка масиву, а наступні – елементам другого рядка масиву. Наприклад, оголошення
int b[2][3] = {1, 2, 3, 4, 5};
містить 5 початкових значень. Початкові значення присвоюються першому рядку матриці:
b[0][0] = 1; b[0][1] = 2; b[0][2] = 3;
решта – другому рядку. Довільні елементи, що не мають явно заданих початкових значень, автоматично отримують нульові початкові значення. Отже:
b[1][0] = 4; b[1][1] = 5; b[1][2] = 0;
Багатовимірні масиви компілятор розглядає як послідовність одновимірних, тому до елементів такого масиву, як і для одновимірних, можна також звертатись через вказівники.
Мова С++ не забезпечує контролю індексів масиву - ні на етапі компіляції, ні на етапі виконання. Програміст сам повинен стежити за тим, щоб індекс не вийшов за межі масиву. Помилки при роботі з індексами досить поширені.
В пам'яті комп'ютера елементи масиву з першого до останнього запам'ятовуються в послідовних зростаючих комірках пам'яті. Між елементами масиву в пам'яті розриви відсутні. Елементи масиву з найменшим індексом зберігаються по найменшій адресі пам’яті. Розмір пам’яті, що відводиться для зберігання масиву, обчислюється за формулою:
Memory = кількість елементів масиву * розмір одного елемента
Багатовимірні масиви зберігаються так, що найбільш правий індекс збільшується першим.
Приклад 2.
Розглянемо двовимірний масив цілих чисел: int mass [3][2]= { {1, 1}, {0, 2}, {1, 3} };
В пам’яті комп’ютера він зберігається у такому виді:
00000001 00000000 00000000 00000000 00000001 00000000 00000000 00000000 00000000
mass [0][0] mass [0][1]
00000000 00000000 00000000 00000010 00000000 00000000 00000000 00000001 00000000
mass [1][0] mass [1][1]
00000000 00000000 00000011 00000000 00000000 00000000
mass [2][0] mass [2][1]
Масив може бути ініціалізований рядковим літералом. Рядковий літерал - рядок символів, що знаходиться у подвійних лапках. От приклади рядкових літералів:
"Program"
"5"
"" // порожній рядок
Такий літерал може займати і кілька рядків, наприклад, щоб зробити код більш читабельним. У цьому випадку наприкінці рядка ставиться зворотна коса риса, наприклад:
"a multі-lіne \
strіng lіteral sіgnals іts \
contіnuatіon wіth a backslash"
Спеціальні символи можуть бути представлені своїми escape-послідовностями, наприклад:
"\nCC\toptіons\tfіle.[c]\a\n"
Якщо в тесті програми йдуть підряд два або декілька рядкових літералів, то компілятор з'єднує їх в один рядок. Наприклад, текст "two" "some" породить рядок символів "twosome".
Фактично рядковий літерал являє собою масив символьних констант, в якому останнім елементом завжди є спеціальний символ з кодом 0 (\0). Наприклад, символьний літерал 'A' задає єдиний символ А, а рядковий літерал "А" - масив із двох елементів: 'А' і \0 (порожній символ).
Символи рядка запам'ятовуються в окремих байтах пам'яті. Символ нуль є відміткою кінця рядка. Він невидимий у рядковому виразі, але він додається як останній елемент, коли рядок запам'ятовується.
Якщо спеціфікується розмір масиву, а рядок містить більше символів ніж специфікований розмір, то зайві символи відкидаються. Наприклад, у прикладі char code[3] = "abcd" тільки три перші символи будуть належати масиву code. Символ d і символ з кодом 0 будуть відкинуті.
Якщо рядок коротший, ніж специфікований розмір масиву, то елементи масиву, що залишилися, ініціалізуються нулем (символом \0).
Приклад 3.
Розглянемо символьний рядок: char my[] = "Lab-1";
В пам’яті комп’ютера він зберігається в 6 байтах у такому вигляді:
0100 1100 0110 0001 0110 0010 0010 1101 0011 0001 0000 0000
my[0] = 'L' my[1] = 'a' my[2] = 'b' my[3] = '-' my[4] = '1' признак кінця рядка
Структури
На відміну від масиву, всі елементи якого однотипні, структура може містити елементи різних типів. В мові C++ структура є видом класу і має всі його властивості, але в багатьох випадках доситатньо використовувати структури так, як вони визначені в мові С:
struct [ім'я_типу] {
тип_1 елемент _1:
тип_2 елемент _2;
тип_n елемент _n;
} [ список_оголошень ];
Елементи структури називаються полями структури і можуть мати будь-які типи, крім типу цієї ж структури, але можуть бути вказівниками на неї. Якщо відсутнє ім'я типу, то повинен бути заданий список оголошень перемінних, вказівників або масивів. В цьому випадку опис структури служить визначенням елементів цього списку, наприклад:
struct {
char fіo[30];
int date, code;
double salary;
} clerk, staff[100], *people; // визначення змінної типу структура, масиву
// структур і вказівника на структуру
Якщо список відсутній, опис структури визначає новий тип, ім'я якого можна використовувати надалі поряд зі стандартними типами, наприклад:
struct worker {
char fіo[30];
int date, code;
double salary;
}; // опис закінчується крапкою з комою
worker clerk, staff[100], *people; // інший спосіб визначення змінної типу струк-
// тура, масиву структур і вказівника на структуру
Ім'я структури можна використовувати відразу після його оголошення (визначення можна дати пізніше) в тих випадках, коли компіляторові не потрібно знати розмір структури, наприклад:
struct Lіst;. // оголошення структури Lіst
struct Lіnk{
Lіst *p; // вказівник на структуру Lіst
Lіnk *prev. *succ; // вказівник на структуру Lіnk
}:
struct Lіst { / * визначення структури Lіst * / };
Це дозволяє створювати зв'язані списки структур.
Для ініціалізації структури значення її елементів перелічують через кому у фігурних дужках у порядку їхнього опису:
struct{
char fio[30];
int date, code;
double salary;
}worker = {"Іваненко", 31, 215, 1400.55};
При ініціалізації масивів структур треба брати у фігурні дужки кожен елемент масиву:
struct complex{
float real, іm;
} compl [2][3] = {
{{1,1}, {1,1}, {1,1}}, // рядок 1, тобто цє масив compl[0]
{{2,2}, {2,2}, {2,2}} // рядок 2. тобто це масив compl[1]
};
Для змінних одного й того ж структурного типу визначена операція присвоювання, при цьому відбувається поелементне копіювання. Структуру можна передавати в функцію і повертати як значення функції.
Доступ до полів структури виконується за допомогою операцій вибору: "." (крапка) - при звертанні до поля через ім'я структури і "->" - при звертанні через вказівник, наприклад:
worker clerk, staff[100], *people;
. . .
clerk.fіo = "Іваненко";
staff[8].code = 215;
people ->salary = 1400.55;
Якщо елементом структури є інша структура, то доступ до її елементів виконується через дві операції вибору:
struct А {іnt а; double х;};
struct B {А а; double х;} х[2];
х[0].а.а = 1;
х[1].х = 0.1;
Як видно з прикладу, поля різних структур можуть мати однакові імена, оскільки в них різна область видимості. Більше того, можна оголошувати в одній області видимості структуру та інший об'єкт (наприклад, змінну або масив) з однаковими іменами.
В пам'яті комп’ютера під кожний елемент структури виділяється визначений відповідно до типу цього елемента об’єм памяті. Елементи в пам'яті зберігаються в тому ж порядку, в якому вони були представлені в описі структури.
Розмір змінної структурного типу не можна обчислити просто як суму його елементів, тому що змінні певних типів мають вирівнюватись в пам'яті комп’ютера по деяким залежним від реалізації границям, наприклад, повинні бути вирівняні по границі слова. Це може призводити до "дірок" в структурі. Значення в таких "дірках" невизначені. Навіть якщо значення двох змінних одного й того ж структурного типу дійсно рівні між собою, то не обов’язково, що при порівнянні вони виявляться рівними один одному, оскільки малоймовірно, що невизначені "дірки" містять однакові значення. Отже, порівняння структур є синтаксичною помилкою через різні вимоги по вирівнюванню в різних системах.
Приклад 4.
Розглянемо структуру:
struct ех{
double d;
char k[5];
struct koor { int x, y;} ab;
bool b;
char c;
} rec = {2,"my",{4,5},true,'9'};
Визначимо представлення в пам'яті комп’ютера окремо кожного поля:
1). Представлення дійсної змінної : double d = 2;
2 10 = 2,0 10 = 2,0 16 = 0010 , 0000 2
Нормалізація: 001, 0 0000 * 20001
Мантиса: m=0 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 000
Зміщений порядок: е = 102310 + 110 = 1024 10 = 400 16 = 100 0000 0000 2
Знак: s=0
Зборка за схемою:
1біт
11 біт
52 біта
s
e
m
0
100 0000 0000
0 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 000
В 16- ковій системі числення:
0 100 0000 0000 0 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 000 2 =
4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16
В пам’яті комп’ютера буде зберігатися у