Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

C _Учебник_МОНУ

.pdf
Скачиваний:
199
Добавлен:
12.05.2015
Размер:
11.12 Mб
Скачать

Вказівники. Динамічна пам’ять

209

 

 

Звернімо увагу на те, що “зірочки” при оголошенні вказівника та операції розадресації – це зовсім різні речі, які лише позначаються однаковим символом.

Вказівник, який не вказує на жодне значення, називається порожнім, чи нульовим. Такий вказівник має значення 0 чи NULL.

Вказівники використовуються при роботі з динамічними змінними і при передаванні параметрів до функцій (див. розд. 8).

6.2 Вказівники на одновимірні масиви

Масиви і вказівники у С++ тісно пов‟язані і можуть використовуватись майже еквівалентно. Ім‟я масиву можна сприймати як константний вказівник на перший елемент масиву. Його відмінність від звичайного вказівника полягає у тому, що його неможна модифікувати.

Здійснимо оголошення масиву mass з п‟яти цілих чисел з ініціалізацією значень елементів і вказівника на ціле ptr:

int mass[5] = {10,-2,0,40,3}, *ptr;

При такому оголошенні масиву пам‟ять виділяється не лише для п‟яти елементів масиву, а й для вказівника з ім‟ям mass, значення якого дорівнює адресі першого елемента масиву mass[0], тобто сам масив залишається безіменним, а доступ до елементів масиву здійснюється через вказівник з ім‟ям mass.

10

–2

0

40

3

*mass *(mass+1)*(mass+2)*(mass+3)*(mass+4)

Для того, щоби звернутися до 3-го елементу цього масиву, можна записати чи то mass[2] чи *(mass+2). При реалізації на комп‟ютері перший з цих способів зводиться до другого, тобто індексний вираз перетвориться до адресного. Оскільки операції над вказівниками виконуються швидше, тому, якщо елементи масиву обробляються почергово, то доцільніше використовувати другий спосіб. Якщо ж вибір елементів є випадковим, то, щоб уникнути помилок, прийнятнішим є перший спосіб. Крім того, перший спосіб є більш наочним і звичним для сприйняття, що сприяє кращій читабельності програм.

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

ptr = mass;

Цей запис є еквівалентним присвоюванню адреси першого елемента масиву (тобто до елемента з нульовим індексом)у вигляді оператора

ptr = &mass[0];

 

10

 

 

–2

 

0

40

3

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ptr

 

ptr+1

 

ptr+2

 

ptr+3

 

 

ptr+4

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Після цього звернутися до першого елемента масиву і надати йому значення 2 можна будь-яким з шести операторів:

* mass = 2;

mass[0] = 2;

*(mass+0) = 2;

210

Розділ 6

* ptr = 2;

ptr[0] = 2;

*(ptr+0) = 2;

Усі ці оператори за дією є тотожними, але швидше за всі виконуватимуться присвоювання *mass = 2 та *ptr = 2, оскільки в них не треба виконувати операції додавання.

Вказівники можна індексувати так само, як і масиви. Наприклад, вираз ptr[3] посилається до четвертого елемента масиву mass[3].

Зауважимо, що не слід плутати такі оголошення:

int

*p1[10];

//

приклад 1

int

(*p2)[10];

//

приклад 2

Упершому прикладі оголошено масив вказівників з ім‟ям p1. Масив складається з 10 елементів, кожний з яких є вказівником на змінну типу int.

Удругому прикладі оголошено змінну-вказівник з ім‟ям p2, яка вказує на масив з 10 цілих чисел типу int.

6.3Арифметика вказівників

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

З вказівниками може виконуватися обмежена кількість арифметичних операцій. Вказівник можна збільшувати (++), зменшувати (– –), додавати до вказівника цілі числа (+ чи +=), віднімати від нього цілі числа (– чи – =) або віднімати один вказівник від іншого.

Здебільшого арифметичні операції застосовуються до вказівників на масиви. Більше того, арифметика вказівників втрачає всякий сенс, якщо вона виконується не над вказівниками на масив. Змінення (збільшення/зменшення) значення вказівника на масив по суті є зсувом адреси у межах цього масиву. Перетворення цілої величини до адресного зсуву припускає, що у межах зсуву щільно розташовані елементи однакового розміру. Це припущення справедливо для елементів масиву, оскільки масив визначається як набір величин однаково типу, а його елементи розташовані у суміжних комірках пам‟яті. Додавання і віднімання адрес, які посилаються на будь-які величини, крім елементів масиву, дає непередбачуваний результат, оскільки в такому разі не гарантується щільне заповнення пам‟яті.

Розглянемо на прикладах дію арифметичних операцій на вказівники:

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

ptr += 3;

збільшить адресу, яка міститься у ptr, на 4*3=12 байти, і ptr вказуватиме на четвертий елемент масиву зі значенням 40.

Загальна формула, яка обчислює значення вказівника при додаванні (відніманні) цілого числа N:

Вказівники. Динамічна пам’ять

211

 

 

<Нова адреса> = <стара адреса> +/- <розмір базового типа>*N;

Операції інкремента (++) і декремента (--), тобто збільшення чи зменшення вказівника на 1. При цьому адреса, яка зберігається у цьому вказівнику, збільшується на розмір базового типу в байтах. Наприклад, команда

ptr++;

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

Зменшення вказівника на 1 означає віднімання розміру в байтах базового типу від адреси, яка зберігається у вказівнику.

Різниця однотипних вказівників. Вказівники можна віднімати один від одного. Наприклад, якщо ptr1 вказує на перший елемент масиву mass, а вказівник ptr2 – на четвертий, то результат виразу ptr2 –ptr1 має тип int і дорівнює 3 – різниці індексів елементів, на які вказують ці вказівники. І так буде, не дивлячись на те, що адреси, які містяться у цих вказівниках, розрізняються на 12 байтів (якщо елемент масиву займає 4 байти). Тобто, результат такої операції дорівнює кількості елементів вихідного типу між зменшуваним і від‟ємником, причому якщо перша адреса молодше, то результат має від‟ємне значення.

Загальна формула при відніманні однотипних вказівників:

<Кількість елементів>=(<перший вказівник><другий вказівник>)/<розмір типу>

Наприклад:

int *ptr1, *ptr2, mass[5]={10,-2,0,40,3}, i;

ptr1 = a + 4;

 

 

 

 

ptr2 = a + 9;

 

 

 

 

i = ptr1 - ptr2;

/*

дорівнює

–5

*/

i = ptr2 - ptr1;

/*

дорівнює

5

*/

При порівнянні вказівників порівнюються адреси, які містяться у вказівниках, а результат порівняння дорівнює 0 (false) або 1 (true). Порівняння вказівників операціями “>”, “<”, “>=”, “<=” мають сенс лише для вказівників на один той самий масив. Проте, операції відношення “==” та “!= ” мають сенс для будь-яких вказівників. При цьому вказівники є рівнозначними, якщо вони вказують на одну і ту саму адресу в пам‟яті. Наприклад, для розглянутих вище вказівників ptr1 та ptr2 у команді

if(ptr1 > ptr2) mass[3]=4;

значення ptr1 є менше за значення ptr2 і тому оператор mass[3]=4 не буде виконаний.

Приклад порівняння вказівника ptr з ненульовим значенням: if(ptr != NULL) ShowMessage("Ненульовий вказівник");

Приклад 6.1 В одновимірному масиві з 8 цілих чисел обчислити кількість додатних та суму парних елементів.

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

212

Розділ 6

зультатів. Інший результат передаватимемо з функції через оператор return. Звернення *(a+i) є аналогічним до a[i] і тому можна однаково використовувати як те, так і те.

Текст функції та основної програми: int fun(int *a, int n, int &s)

{int k=0; s=0;

for(int i=0; i<n; i++)

{if (*(a+i) > 0) k++;

if (*(a+i)%2 == 0) s += *(a+i);

}

return k; }

void __fastcall TForm1::Button1Click(TObject *Sender)

{int i, a[8], sum, kol; for(i=0; i<8; i++)

*(a+i) = StrToInt(Memo1->Lines->Strings[i]); kol = fun(a,8,sum);

Edit1->Text = IntToStr(kol);

Edit2->Text = IntToStr(sum);

}

6.4 Вказівники на багатовимірні масиви

Вказівники на багатовимірні масиви у мові С++ – це масиви масивів, тобто такі масиви, елементами яких є масиви. При оголошені таких масивів у пам‟яті комп‟ютера створюється декілька різних об‟єктів. Приміром, при виконанні оголошення двовимірного масиву int arr[4][3] у пам‟яті виділяється ділянка для зберігання значення змінної arr, яка є вказівником на масив з чотирьох вказівників. Для цього масиву з чотирьох вказівників теж виділяється пам‟ять. Кожний з цих чотирьох вказівників містить адресу масиву з трьох елементів типу int, і, отже, у пам‟яті комп‟ютера виділяється чотири ділянки для зберігання чотирьох масивів чисел типу int, кожна з яких складається з трьох елементів. Такий розподіл пам‟яті показано на схемі:

arr

 

 

 

 

 

arr[0]

 

arr[0][0]

arr[0][1]

arr[0][2]

arr[1]

 

arr[1][0]

arr[1][1]

arr[1][2]

arr[2]

 

arr[2][0]

arr[2][1]

arr[2][2]

arr[3]

 

arr[3][0]

arr[3][1]

arr[3][2]

Отже, оголошення arr[4][3] породжує в програмі три різних об‟єкти: вказівник з ідентифікатором arr, безіменний масив з чотирьох вказівників і безіменний масив з дванадцяти чисел типу int. Для доступу до безіменних масивів використовується адресні вирази з вказівником arr. Доступ до елементів масиву вказівників здійснюється із зазначенням одного індексного виразу у формі arr[2] чи *(arr+2). Для доступу до елементів двовимірного масиву чисел типу

Вказівники. Динамічна пам’ять

213

 

 

int має бути використано два індексні вирази у формі arr[1][2] чи еквівалент-

них їй *(*(arr+1)+2) та (*(arr+1))[2].

Нагадаємо, що елементи двовимірних масивів розміщуються у пам‟яті підряд і між його рядками немає ніяких проміжків (див. п. 5.3.1). Такий порядок дає можливість звертатися до будь-якого елементу багатовимірного масиву, використовуючи адресу його початкового елемента та лише один індексний вираз. Так для матриці arr звертання *(arr) є посиланням на елемент arr[0][0], звертання *(arr+2) – на елемент arr[0][2], а звертання *(arr+3) – на елемент arr[1][0] і т. д. Тобто, застосовуючи оператори циклу, можна організувати поелементне опрацювання елементів матриці, приміром введення усієї матриці з компонента StringGrid1:

for(int i=0;i<4;i++) for(int j=0;j<3;j++)

*(arr+i*4+j)=StrToInt(StringGrid1->Cells[j][i]);

Тут вираз *(arr+i*4+j) є аналогічний до arr[i][j].

Розміщення тривимірного масиву відбувається аналогічно й оголошення float W[3][4][5];

породжує у програмі крім самого тривимірного масиву з шістдесяти елементів типу float масив з чотирьох вказівників на тип float, масив з трьох вказівників на масив вказівників на тип float, і вказівник на масив масивів вказівників на тип float.

Для звертання до елемента W[2][3][4] тривимірного масиву також можна використовувати вказівник, оголошений як float *p=W[0][0] з одним індекс-

ним виразом у формі p[2*4*5+3*5+4] чи p[59].

Приклад 6.2 Увести матрицю цілих чисел розміром 4 4 і транспонувати її.

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

П е р ш и й с п о с і б р о з в ‟ я з а н н я

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

Унаведеній тут функції транспонування матриці transp() матриця передається до функції як вказівник і тоді більш звичне звертання до елементів матриці u[i][j] слід замінити виразом *(u+n*i+j) чи u[n*i+j].

Текст функції та її виклику в основній програмі:

214

Розділ 6

const int n=4;

// Функція транспонування матриці void transp(int *u)

{int i,j; for(i=0;i<n-1;i++) for(j=i+1;j<n;j++)

{int p=*(u+n*i+j); *(u+n*i+j)=*(u+n*j+i); *(u+n*j+i)=p;

} }

void __fastcall TForm1::Button1Click (TObject *Sender)

{ int i, j, a[n][n]; for(i=0;i<n;i++) for(j=0;j<n;j++)

a[i][j]=StrToInt(StringGrid1->Cells[j][i]); transp(&a[0][0]);

for(i=0;i<n;i++)

for(j=0;j<n;j++) StringGrid2->Cells[j][i]=IntToStr(a[i][j]);

}

Д р у г и й с п о с і б р о з в ‟ я з а н н я

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

void transp(int (*u)[4], int n);

У цьому запису дужки є обов‟язковими, оскільки оголошення int *u[4] є масивом чотирьох вказівників на данні типу int, а параметр функції не може бути масивом. Існує альтернативний формат запису цього прототипу, який має таке саме тлумачення, але сприймається для розуміння краще:

void transp(int u[][4], int n);

Обидва варіанти означають, що параметр u є вказівником на масив чотирьох значень типу int, а не безпосередньо масивом. Число 4 у квадратних дужках задає кількість стовпців, а кількість рядків можна передавати окремим параметром.

Текст функції та її виклику в основній програмі:

void transp(int (*u)[4], int n) { for(int i=0;i<n-1;i++)

for(int j=i+1;j<n;j++)

{int p=u[i][j]; u[i][j]=u[j][i]; u[j][i]=p;

}

}

void __fastcall TForm1::Button1Click(TObject *Sender)

Вказівники. Динамічна пам’ять

215

 

 

{const int n=4; int i, j, a[n][n]; for(i=0;i<n;i++) for(j=0;j<n;j++)

a[i][j]=StrToInt(StringGrid1->Cells[j][i]); transp(a, n);

for(i=0;i<n;i++)

for(j=0;j<n;j++)

StringGrid2->Cells[j][i]= IntToStr(a[i][j]);

}

Третій різновид передавання до функції двовимірних масивів, зазначений у п. 5.3.4 наведено у розд. 6.7 після того як буде розглянуто оператори динамічного виділення пам‟яті для таких масивів.

6.5 Динамічна пам’ять

Окрім звичайної пам‟яті (стека), в якій автоматично розміщуються змінні за їхнього оголошення, існує ще й динамічна пам‟ять (heap − купа), в якій змінні можуть розміщуватися динамічно. Це означає, що пам‟ять виділяється під час виконання програми, й лише тоді, коли у програмі зустрінеться спеціальна інструкція. Основна потреба у динамічному виділенні пам‟яті виникає, коли розмір або кількість даних заздалегідь є невідомі, а визначаються в процесі виконання програми.

У С++ існує кілька команд для виділення динамічної пам‟яті. Найчастіше використовується оператор new, який у загальному вигляді записується як:

<тип> *<ім‟я_вказівника> = new <тип>;

Приклади:

1) int *p = new int;

Тут виділяється місце в пам‟яті під ціле число й адреса цієї ділянки пам‟яті записується у змінну-вказівник p. Звернутися до цього числа можна буде через вказівник на нього: *p = 2;

2) int *p = new int (5);

Ця команда не лише виділить місце у пам‟яті під ціле число, а й запише в цю ділянку пам‟яті значення 5. Адреса першої комірки виділеної ділянки пам‟яті присвоюється змінній-вказівнику p. Звернутися до числа, на яке вказує p, можна аналогічно до попереднього прикладу. Приміром, щоб збільшити таке число на 2, слід написати: (*p) += 2;

3)int *p = new int [5];

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

чисел можна за його номером: p[0], p[1] і т. д. або через вказівник: *p − те ж саме, що p[0]; *(p+1) − те ж саме, що p[1] і т. д.

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

216

Розділ 6

ціальної команди. При виділенні пам‟яті за допомогою оператора new, для звільнення пам‟яті використовується delete:

delete <вказівник>;

Наприклад: delete a;

Якщо оператором new було виділено пам‟ять під кілька значень водночас (як у розглянутому вище прикладі 3), використовується форма команди delete

delete []<вказівник>;

Квадратні дужки повинні бути порожніми, операційна система контролює кількість виділеної пам‟яті і при звільненні їй відома потрібна кількість байтів. Наприклад:

delete [] p;

Окрім операторів new та delete, існують функції, які перейшли до С++ з С, але вони використовуються на практиці набагато рідше.

Функція

void *malloc(size_t size);

(від англ. “memory allocation” – виділення пам‟яті) робить запит до ядра операційної системи щодо виділення ділянки пам‟яті заданої кількості байтів. Єдиний аргумент цієї функции size − кількість байтів, яку треба виділити. Функція повертає вказівник на початок виділеної пам‟яті. Якщо для розміщення заданої кількості байтів є недостатньо пам‟яті функція malloc() повертає NULL. Вміст ділянки лишається незмінним, тобто там може залишитися “бруд”. Якщо аргумент size дорівнює 0, функція повертає NULL. Наприклад, команда

int *a = (int*) malloc(sizeof(int));

виділяє пам‟ять під ціле число й адресу початку цієї ділянки пам‟яті записує у вказівник а.

Виділення пам‟яті під 10 дійсних чисел за допомогою цієї функції: float *a = (float*) malloc(sizeof(float)*10);

Функція

void * calloc(size_t num, size_t size);

виділяє блок пам‟яті розміром num size (під num елементів по size байтів кожен) і повертає вказівник на виділений блок. Кожен елемент виділеного блока ініціалізується нульовим значенням (на відміну від функції malloc). Функція calloc() повертає NULL, якщо не вистачає пам‟яті для виділення нового блока, або якщо значення num чи size дорівнюють 0.

Виділення пам‟яті під 10 дійсних чисел за допомогою функції calloc(): float *a = (float*) calloc(10,sizeof(float));

Функція

void *realloc(*bl, size);

Вказівники. Динамічна пам’ять

217

 

 

змінює розмір виділеного раніш блока пам‟яті з адресою *bl на новий обсяг, який становитиме size байтів. Якщо змінення відбулося успішно, функція realloc() повертає вказівник на виділену ділянку пам‟яті, а інакше повертається NULL. Якщо *bl є NULL, функція realloc() виконує такі самі дії, що й malloc(). Якщо size є 0, виділений за адресою *bl блок пам‟яті звільнюється – і функція повертає NULL. Для ілюстрації роботи функції realloc() наведемо приклад змінення розміру раніш виділеної пам‟яті під одновимірний масив з 10-ти дійсних елементів до 20-ти елементів:

a = (float*) realloc(a, 20);

Пам‟ять, виділену за допомогою функцій malloc() і сalloc(), звільнюють за допомогою функції free():

void free(*bl);

де *bl – адреса початку виділеної раніше пам‟яті, наприклад: free(а);

Використання згаданих функцій разом з вказівниками надає можливість керувати розподілом динамічної пам‟яті.

Якщо не звільняти динамічно виділену пам‟ять, коли вона стає більш не потрібною, у системі може виникнути нестача вільної пам‟яті. Іноді це називають «витіком пам‟яті».

6.6 Динамічні одновимірні масиви

Відмінності динамічного масиву від звичайного полягають у тому, що:

память під динамічний масив виділяється динамічно за допомогою вищерозглянутих функцій;

кількість елементів динамічного масиву може бути задано змінною (але

упрограмі її неодмінно має бути визначено до виділення пам‟яті під масив). Синтаксис оголошення динамічного одновимірного масиву за допомогою

оператора new є такий:

<базовий тип> *<ім‟я>=new <базовий тип>[<кількість елементів>];

Приклад оголошення дійсного динамічного масиву зі змінною кількістю елементів:

int N; N=StrToInt(Edit1->Text); float *a=new float [N];

Тут кількість елементів масиву є змінною і вводиться з клавіатури з Edit1 перед оголошенням масиву. Звільнення пам‟яті від цього масиву a матиме вигляд:

delete []a;

Синтаксис оголошення динамічного одновимірного масиву за допомогою функції malloc() є такий:

<тип> *<ім‟я> = (<тип>*) malloc(sizeof(<тип>)*<кільк. ел.>);

218

Розділ 6

Приклад оголошення дійсного динамічного масиву зі змінною кількістю елементів за допомогою функції malloc():

int N; N=StrToInt(Edit1->Text);

float *a=(float *)malloc(sizeof(float)*N);

Приклад оголошення дійсного динамічного масиву зі змінною кількістю елементів за допомогою функції calloc():

int N; N=StrToInt(Edit1->Text);

float *a=(float *)calloc(sizeof(float), N);

Звільнення пам‟яті з-під цього масиву a: free (a);

Приклади програм з динамічними масивами

Приклад 6.3 Увести дійсні числа у Memo і створити з них масив ненульових чисел. Обчислити кількість від‟ємних елементів цього масиву з непарними індексами.

Розв‟язок. Оскільки заздалегідь невідомо кількість чисел, які будуть введені у Memo1, тому має сенс організувати динамічний масив. При розв‟язанні подібних завдань зазвичай дотримуються такої послідовності дій:

1)Визначити кількість елементів майбутнього масиву. У даному прикладі для цього треба зчитати послідовно всі числа з Memo і обчислити кількість ненульових з них.

2)Оголосити динамічний масив і виділити під нього пам‟ять, використо-

вуючи чи то new, чи malloc(), чи calloc().

3)Заповнити масив значеннями. У розглядуваному прикладі для цього слід ще раз послідовно зчитати всі числа з Memo і присвоїти елементам масиву лише ненульові з цих чисел. Слід звернути увагу, що при заповнюванні масиву числами з Memo1, індекс елемента масиву, який заповнюється на поточному кроці, й індекс рядка Memo1, з якого зчитується поточне число, не збігаються. Тому для індексів масиву створено окрему змінну j.

4)Вивести створений масив. У наведеній нижче програмі виведення масиву організовано у StrinGrid1, який при створюванні форми для зручності перейменовано на SG1.

Наведемо два варіанти розв‟язання цього завдання з використанням оператора new та функції malloc().

Перший спосіб розв‟язання з використанням оператора new: void __fastcall TForm1::Button1Click(TObject *Sender)

{ int

i, j=0;

// i – індекс числа Memo1, j – індекс елемента масиву,

int

k=0;

// змінна k для кількості від‟ємних елементів масиву.

int

n=Memo1->Lines->Count; // Кількість чисел в Memo1

int

N=0; float x;

 

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]