- •12. Класи пам'яті даних 100
- •13. Робота з файлами 106
- •14. Посилання 111
- •Склад мови
- •Алфавіт мови
- •Лексеми
- •Ключові слова
- •Ідентифікатори
- •Константи
- •Символьні рядки
- •Знаки операцій, роздільники, коментарі
- •Типи даних
- •Класифікація типів даних
- •Цілочислові типи
- •Дійсні типи
- •Оголошення змінних і констант
- •Переліки
- •Структура програми
- •Функція main
- •Область дії змінної
- •Введення і виведення даних
- •Функції стандартної математичної бібліотеки
- •Вирази і операції
- •Поняття виразу, операнда, операції
- •Порядок виконання операцій
- •Арифметичні операції
- •Порозрядні операції
- •Операції порівняння
- •Логічні операції
- •Операції присвоєння
- •Умовна операція
- •Операція визначення розміру sizeof
- •Узгодження типів у виразах
- •Умовні оператори
- •Оператори
- •Умовний оператор if
- •Оператор вибору switch
- •Оператори циклу
- •Цикл з параметром. Оператор for
- •Цикл з передумовою. Оператор while
- •Цикл з постумовою. Оператор do while
- •Оператори переходу
- •Використання псевдовипадкових чисел
- •Одновимірні масиви
- •Багатовимірні масиви
- •Символьні рядки
- •Вказівники
- •Оголошення вказівника, операції, пов’язані з вказівниками
- •Адресна арифметика
- •Void – вказівники, типізація вказівників
- •Звертання до елементів масивів через вказівники
- •Масиви символьних рядків і масиви вказівників
- •Динамічне виділення пам’яті
- •Структури і об’єднання
- •Оголошення і ініціалізація структур
- •Звертання до елементів структур
- •Перейменування типів
- •Об’єднання
- •Поля бітів
- •Директиви препроцесора
- •Призначення директив препроцесора
- •Директива включення #include
- •Директиви макропідстановок #define I #undef
- •Директиви умовної компіляції
- •Директиви #ifdef, #ifndef
- •Функції
- •Структура функції
- •Виклик функцій. Прототипи функцій
- •Взаємодія фактичних і формальних параметрів функцій
- •Inline – функції
- •Масиви і символьні рядки як параметри функцій
- •Використання кваліфікатора const в оголошеннях параметрів
- •Багатовимірні масиви як параметри функцій
- •Опрацювання структур у функціях
- •Вказівники на функції
- •Рекурсивні функції
- •Класи пам'яті даних
- •Клас пам'яті, час існування і видимість об’єкта
- •Область дії глобальних і локальних змінних
- •Специфікатори класів пам'яті
- •Специфікатори глобальних змінних
- •Багатофайлові програми
- •Робота з файлами
- •Звертання до файлів
- •Послідовний запис до файлу і послідовне читання з файлу
- •Файли з довільним доступом
- •Посилання
- •Призначення посилань
- •Передача аргументів функцій як посилань
Void – вказівники, типізація вказівників
Вказівникам можна присвоювати адреси тільки тих даних, тип яких збігається з базовим типом вказівника. Виняток становлять спеціальні не типізовані вказівники, оголошені з ключовим словом void. Такі вказівники називають не типізованими, вони сумісні з усіма вказівниками програми і адресами усіх об’єктів.
При виконання операції звертання за адресою через не типізовані вказівники необхідно явно задавати тип значення, яке зберігається в пам'яті. Наприклад:
#include <iostream>
using namespace std;
int main() {
int a[ 5 ] = { 1, 2, 3, 4, 5 };
int * pi;
void * pv;
pi = & a[ 0 ];
pv = pi;
cout << *( int * )pv;
*(( int * ) pv + 1 ) = 23;
cout << a[ 1 ];
}
Операції перетворення до заданого типу і звертання за адресою мають однаковий пріоритет, виконуються справа наліво. Тобто, при обчисленні виразу *( int * )pv спочатку вказівник на void перетворюється в вказівник на int, після цього виконується звертання за отриманою адресою. Так само, при обчисленні виразу *(( int * ) pv + 1 ) спочатку вказівник на void перетворюється в вказівник на int, після цього значення вказівника збільшується на одиницю – тепер вказівник посилається на другий елемент масиву. Останньою виконується операція звертання за адресою, в цьому прикладі ми отримуємо адресу, по якій буде записано значення 23.
Звертання до елементів масивів через вказівники
В мові програмування С++ ім’я масиву є константним вказівником на його першій елемент. Це дозволяє звертатися до елементів масиву не тільки за допомогою індексів, але і за допомогою вказівників. При цьому, при використанні вказівників теж допускається використання квадратних дужок і індексів. Тобто, наступні звертання до елементів масиву будуть еквівалентними:
#include <iostream>
using namespace std;
int main() {
int a[ 5 ] = { 1, 2, 3, 4, 5 };
int * p = a;
cout << a[ 1 ] << endl;
cout << * ( a + 1 ) << endl;
cout << p[ 1 ] << endl;
cout << * ( p + 1 ) << endl;
p = & a[ 1 ];
cout << * p << endl;
}
Для роботи з масивами зручно використовувати додаткові вказівники, які посилаються на елементи масиву, що опрацьовуються в даний момент. Знайдемо середнє значення елементів масиву з 100 випадкових дійсних чисел за допомогою вказівників:
#include <iostream>
#include <ctime>
using namespace std;
int main() {
srand( unsigned( time ( NULL ) ) );
const int NUM = 100;
double a[ NUM ], avg, * p;
double * plast = & a[ NUM - 1 ];
for ( int i = 0; i < NUM; ++i ) {
a[ i ] = ( rand( ) % 1001 ) / 1000.;
}
for ( p = a, avg = 0; p <= plast; p++ )
avg += * p;
avg /= NUM;
cout << avg << endl;
}
Такий підхід до організації циклів зручно використовувати при опрацюванні рядків символів. При цьому ознакою виходу із циклу є посилання вказівника на нуль – символ. Наприклад, є рядок символів. Символи поділені на такі групи: 1) символи операцій ‘+’, ‘-’, ‘*’, ‘/’; 2) символи пунктуації: ‘,’, ‘.’, ‘;’; 3) інші символи. Знайти кількість символів кожної групи в рядку.
#include <iostream>
using namespace std;
int main() {
char c[ ] = "+-*/ 1234 .,; +-*/.,";
char * p = c;
int n[ 3 ] = { };
do
switch ( * p ) {
case '+':
case '-':
case '*':
case '/': n[ 0 ]++; break;
case '.':
case ',':
case ';': n[ 1 ]++; break;
default: n[ 2 ]++;
}
while ( * ++p );
for ( int i = 0; i < 3; i++ )
cout << i << " : " << n[ i ] << endl;
}
У виразі * ++p операції інкремента і звертання за адресою мають однаковий пріоритет, асоціативність справа наліво. Тобто, спочатку виконується збільшення вказівника на одиницю, тепер він посилається на наступний символ рядка. Після цього виконується операція звертання за адресою, яка повертає символ рядка. Якщо цим символом є нуль – символ, то виконання циклу завершується.
Елементи багатовимірних масивів займають неперервну ділянку оперативної пам'яті, адресу початку якої зберігає константний вказівник – ім’я масиву. Тому, багатовимірний масив можна розглядати як одновимірний, в якому кожен елемент має свій порядковий номер. Наприклад, елемент двовимірного масиву з індексами i, j буде мати порядковий номер i * кількість_елементів_в_рядку + j.
#include <iostream>
using namespace std;
int main() {
const int NROWS = 2; // Кількість рядків матриці
const int NCOLS = 3; // Кількість елементів в кожному рядку
int a[ NROWS ][ NCOLS ] = { { 1, 2, 3} , { 4, 5, 6} };
int * p;
p = ( int * ) a;
cout << a[ 1 ][ 2 ] << endl;
cout << * ( p + 1 * NCOLS + 2 ) << endl;
}
Вказівники зручно використовувати у випадках, коли обробка елементів багатовимірних масивів виконується незалежно від положення елемента в цьому масиві. Наприклад, знайдемо максимальний елемент матриці цілих випадкових чисел.
#include <iostream>
#include <ctime>
using namespace std;
int main() {
srand( unsigned( time ( NULL ) ) );
const int NROWS = 3; // Кількість рядків матриці
const int NCOLS = 4; // Кількість елементів в кожному рядку
int a[ NROWS ][ NCOLS ];
for ( int i = 0; i < NROWS; i++ ) {
for ( int j = 0; j < NCOLS; j++ ) {
a[ i ][ j ] = rand( ) % 100;
cout << a[ i ][ j ] << "\t";
}
cout << endl;
}
int * p = ( int * ) a;
int * plast = & a[ NROWS - 1 ][ NCOLS - 1 ];
int max = *p++;
do
if ( * p > max )
max = * p;
while ( ++p <= plast );
cout << max << endl;
}
Для багатовимірних масивів ім’я масиву зберігає адресу першого байти масиву. Але багатовимірні масиви в С++ - це масиви, елементами яких є масиви меншої вимірності. Тобто, двовимірний масив – це одновимірний масив, елементами якого є одновимірні масиви. Ім’я масиву – це константний вказівник на його перший елемент, тобто це вказівник на початок першого одновимірного підмасива. Через це в попередніх прикладах необхідно було виконувати перетворення типів в виразі p = ( int * ) a.
Для масиву а з попереднього приклада, вирази a[ 0 ] i * a виділяють перший елемент масиву а, який є одновимірним масивом з чотирьох елементів цілого типу. Значення a[ 0 ], a[ 1 ], a[ 2 ] є адресами масивів кожного рядка матриці. При цьому, значення адреси, яка зберігається в а, і значення a[ 0 ] однакові, але а вказує на перший рядок матриці, а a[ 0 ] – на перший елемент першого рядка.
Оскільки a[ і ] є константною адресою і-го рядка матриці, то вираз a[ і ] + j обчислює адресу j-го елемента цього рядка. До елементів багатовимірних масивів можна звертатися, використовуючи тільки індексну або тільки вказівникові форму доступу, а також різні комбінації обох форм:
#include <iostream>
using namespace std;
int main() {
const int NROWS = 2; // Кількість рядків матриці
const int NCOLS = 3; // Кількість елементів в кожному рядку
int a[ NROWS ][ NCOLS ] = { { 1, 2, 3} , { 4, 5, 6} };
// Звертання до першого елемента матриці
cout << a[ 0 ][ 0 ] << endl;
cout << * a[ 0 ] << endl;
cout << * * a << endl;
// Звертання до j-го елемента першого рядка матриці
int j = 1;
cout << a[ 0 ][ j ] << endl;
cout << * ( a[ 0 ] + j ) << endl;
cout << * ( * a + j ) << endl;
// Звертання до першого елемента і-го рядка матриці
int i = 1;
cout << a[ i ][ 0 ] << endl;
cout << * a[ i ] << endl;
cout << * * ( a + i ) << endl;
// Звертання до j-го елемента і-го рядка матриці
cout << a[ i ][ j ] << endl;
cout << * ( a[ i ] + j ) << endl;
cout << * ( * ( a + i ) + j ) << endl;
cout << ( * ( a + i ) ) [ j ] << endl;
}
Для звернення до елементів багатовимірних масивів можна також використовувати більш складні оголошення:
#include <iostream>
using namespace std;
int main() {
const int NROWS = 2; // Кількість рядків матриці
const int NCOLS = 3; // Кількість елементів в кожному рядку
int a[ NROWS ][ NCOLS ] = { { 1, 2, 3} , { 4, 5, 6} };
int ( * b )[ NCOLS ];
b = a; // Вказівник на перший рядок матриці
cout << * ( * b + 0 ) << endl;
cout << * ( * b + 1 ) << endl;
cout << * ( * b + 2 ) << endl;
b++; // Вказівник на другий рядок матриці
cout << * ( * b + 0 ) << endl;
cout << * ( * b + 1 ) << endl;
cout << * ( * b + 2 ) << endl;
}
В цьому прикладі оголошення int ( * b )[ NCOLS ]; створює вказівник на масив з NCOLS цілих чисел. В складних оголошеннях такого типу квадратні і круглі дужки справа від ідентифікатора мають пріоритет перед зірочкою зліва від ідентифікатора. Квадратні і круглі дужки мають однаковий пріоритет і розкриваються зліва направо. Специфікатор типу розглядається на останньому кроці, коли оголошення повністю інтерпретоване. Можна використовувати круглі дужки, щоб змінити порядок інтерпретації на необхідний.
Для інтерпретації складних оголошень використовується просте правило, яке складається з чотирьох кроків:
1) почати з ідентифікатора і подивитись направо, чи є там квадратні або круглі дужки;
2) якщо вони є, то виконати інтерпретацію цієї частини оголошення, після цього подивитись наліво, чи є там зірочка;
3) якщо на будь-якій стадії інтерпретації закриваючу круглу дужку, то спочатку необхідно застосувати всі ці правила всередині круглих дужок, і після цього продовжити інтерпретацію;
4) виконати інтерпретацію специфікатора типу.
