Посібник для початківця про вказівники

Що таке вказівники?

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

Приступаємо до роботи

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

int* pNumberOne;
int* pNumberTwo;


Звернули увагу на префікс "p" в обох іменах змінних? Цей прийом використовується для того, щоб показати, що змінна є вказівником. Тепер давайте зробимо так, щоб вказівники вказували на щось:

pNumberOne = &some_number;
pNumberTwo = &some_other_number;


Знак амперсанд (&) слід розуміти як "адреса чого- небудь", він означає, що буде одержана саме адреса змінної у пам'яті, а не значення самої змінної. Таким чином, у цьому прикладі, у pNumberOne зберігається адреса змінної some_number , тому pNumberOne тепер вказує на some_number.

Тепер, якщо потрібно звернутися за адресою змінної some_number, можна використовувати pNumberOne. Для такого звертання слід було б написати *pNumberOne. Кажуть, що оператор (*) розіменовує вказівник. Це значить, що він повертає значення в комірці пам'яті, на яку вказує цей вказівник. За винятком самого оголошення вказівника int *pNumber.

Для закріплення матеріалу: Приклади

Хух! Це досить важко. Якщо ви не зрозуміли, в чому полягає суть вказівників, рекомендую ще раз уважно перечитати матеріал. Вказівники - це складний матеріал, і може знадобитись доволі багато часу, щоб опанувати його. Ось приклад, що демонструє ідеї, які обговорювалися вище. Він написаний на C, без розширених можливостей C++.
#include <stdio.h>

void main()
{
  // оголошення змінних:
  int nNumber;
  int *pPointer;

  // задаємо їхні значення:
  nNumber = 15;
  pPointer = &nNumber;

  // вивід значення nNumber:
  printf("nNumber дорівнює: %d\n", nNumber);

  // змінюємо значення nNumber через pPointer:
  *pPointer = 25;

  // на доказ того, що значення nNumber змінилось після попереднього рядка
  // виведемо цю змінну знову:
  printf("nNumber дорівнює: %d\n", nNumber);
}
Перечитайте та скомпілюйте наведений вище приклад коду. Переконайтеся у тому, що розумієте, як він працює. Коли завершите, можна читати далі.

Пастка! Спробуйте знайти помилку в наведеній нижче програмі:
#include <stdio.h>

int *pPointer;

void SomeFunction();
{
  int nNumber;
  nNumber = 25;

  // зробити pPointer вказівником на nNumber:
  pPointer = &nNumber;
}

void main()
{
  SomeFunction(); // зробити pPointer вказівником на що-небудь

  // чому це не спрацьовує?
  printf("Значення *pPointer: %d\n", *pPointer);
}
Спочатку ця програма викликає функцію SomeFunction, яка створює змінну nNumber, а потім робить pPointer вказівником на неї. Однак є одна проблема. Коли функція завершується, локальні змінні функції видаляються. Локальні змінні завжди видаляються коли виходять з області бачення. Це означає, що коли SomeFunction повертає значення до main() то змінна знищується.

Таким чином pPointer вказує на змінну, яка вже не належить програмі. Якщо ви не розумієте це, потрібно почитати про локальні та глобальні змінні, а також про область бачення. Ця концепція також важлива.

І так, як можна вирішити проблему? Відповідь полягає у використанні методу, відомого як динамічний розподіл. Пам’ятайте, що між С і С++ є різниця. Так як більшість розробниців на даний момент використовують С++, його діалект ми і використаємо нижче.

Динамічний розподіл

Динамічне розподіл, можливо, є ключем до вказівників. Він використовується для виділення пам'яті без необхідності визначення змінних, а потім створює посилання на них. Хоча ця концепція може здатися незрозумілою, але це дійсно просто. Наступний код показує, як виділити пам'ять для цілого числа:

int *pNumber;
pNumber = new int;


У першому рядку оголошується вказівник pNumber. У другому рядку виділяється пам'ять для для цілого числа і pNumber робиться вказівником на цю пам'ять. Ось ще один приклад. Цього разу використовується число з плаваючою точкою подвійної точності:

double *pDouble;
pDouble = new double;

Формула лишається однією і тею ж, так що вам навряд вдасться помилитися. Але динамічне виділення відрізняється тим, що пам'ять, яка була виділена не видаляється після завершення роботи функції або поточного блоку коду. Так що якщо ви перепишете попередній приклад використовуючи динамічне виділення - ви побачите що тепер він працює:
#include <stdio.h>

int *pPointer;

void SomeFunction()
{
  // зробити pPointer вказівником на нове ціле
  pPointer = new int;
  *pPointer = 25;
}

void main()
{
  SomeFunction(); // зробити pPointer вказівником на будь-що

  printf("Значення *pPointer: %d\n", *pPointer);
}
Прочитайте і скомпілюйте наведений приклад. Перевірте, що ви розумієте як і чому він працює. Коли викликається деяка функція, вона виділяє пам'ять і ініціалізує вказівник, щоб він вказував на неї. Цього разу коли функція завершується, виділена пам'ять залишається недоторканою і pPointer все ще вказує на неї. На цьому все про динамічне виділення пам'яті! Переконайтеся, що ви зрозуміли це і читайте далі, щоб дізнатися про інші неприємні нюанси, а також в чому полягає серйозність помилки в коді вище.

Виділення та звільнення пам’яті

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

delete pPointer;

З цим все. Ви повинні бути обережні, оскільки потрібно передавати правильний вказівник, вказівник який вказує саме на ту область пам'яті яку ви виділяли а не на незрозуміле сміття. Спроба звільнити пам'ять (за допомогою delete), яка вже була звільнена небезпечна і може призвести до збою програми. Тому наступний оновлений приклад показує як це робити правильно без даремного витрачання пам'яті:
#include <stdio.h>

int *pPointer;

void SomeFunction()
{
  // зробити pPointer вказівником на нове ціле
  pPointer = new int;
  *pPointer = 25;
}

void main()
{
  SomeFunction(); // зробити pPointer вказівником на будь-що

  printf("Значення *pPointer: %d\n", *pPointer);

  delete pPointer;
}
Різниця в один рядок - це все, що зроблено, але це важливий рядок. Якщо ви не видалите пам'ять, то отримаєте так званий "витік пам'яті", коли пам'ять поступово витікає і не може бути повернута, поки програма не закриється.

Передача вказівників в функції

Можливість передавати їх індекси у функції дуже корисна, і її легко освоїти. Якщо нам потрібна програма, яка отримує число і додає до нього п'ять, ми можемо написати щось схоже на цей код:
#include <stdio.h>

void AddFive(int Number)
{
  Number = Number + 5;
}

void main()
{
  int nMyNumber = 18;

  printf("Мій старий номер: %d\n", nMyNumber);

  AddFive(nMyNumber);

  printf("Мій новий номер: %d\n", nMyNumber);
}
Проте тут проблема в тому, що змінна Number, до якої ми звертаємося всередині функції - це копія змінної nMyNumber, що передається у функцію. Таким чином, рядок Number = Number + 5 додає п'ять до копії змінної, залишаючи оригінальну змінну в main() незмінною. Спробуйте запустити програму, щоб переконатися в цьому.

Щоб позбавитися від цієї проблеми, ми можемо передавати у функцію вказівник на місце в пам'яті, де зберігається число, але тоді ми повинні поправити функцію, щоб вона приймала вказівник замість числа. Для цього змінимо void AddFive(int Number) на void AddFive(int* Number) додаванням зірочки. Тут знову текст програми, з внесеними змінами. Зверніть увагу, на те що ми повинні переконатися, що передаємо у функцію адресу nMyNumber замість самого числа. Це зроблено за допомогою оператора &, який (як ви пам'ятаєте), читається як "отримати адресу".
#include <stdio.h>

void AddFive(int* Number)
{
  *Number = *Number + 5;
}

void main()
{
  int nMyNumber = 18;

  printf("Мій старий номер: %d\n", nMyNumber);

  AddFive(&nMyNumber);

  printf("Мій новий номер: %d\n", nMyNumber);
}
Спробуйте придумати ваш власний приклад, щоб продемонструвати це. Звернули увагу на важливе значення * перед Number у AddFive функції? Це необхідно, щоб повідомити компілятору, що ми хочемо збільшити значення змінної, на яку вказує Number, а не значення самого вказівника. Останнє, на що необхідно звернути увагу відносно функцій, це те, що ви можете повернути з функції значення вказівника, наприклад:

int * MyFunction();

В цьому прикладі MyFunction повертає вказівник на ціле число

Вказівники на класи

Є декілька інших застережень з вказівниками, одним з яких є структура чи клас. Ви можете визначити клас наступним чином:
class MyClass
{
  public:
    int m_Number;
    char m_Character;
};
Потім ви можете визначити змінну типу MyClass наступним чином:

MyClass thing;

Ви повинні це вже знати. Якщо ні, спробуйте прочитати попередню частину. Щоб визначити вказівник на MyClass, ви повинні використовувати:

MyClass *thing;

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

thing = new MyClass;

Тут проблема полягає в тому, як же тоді б ви використовували цей вказівник? Ну, як звичайно, ви б написали thing.m_Number, але ви не можете це зробити з вказівником, оскільки thing не є MyClass, а вказівником на нього. Таким чином, thing сама по собі не містить змінну m_Number, вона являє собою структуру, яка вказує на те, що містить m_Number. Тому ми повинні використовувати різні конвенції. Це замінить .(dot) на -> (тире і потім знак більше). Приклад можете побачити нижче:
class MyClass
{
  public:
    int m_Number;
    char m_Character;
};

void main()
{
  MyClass *pPointer;

  pPointer = new MyClass;
  pPointer->m_Number = 10;
  pPointer->m_Character = 's';

  delete pPointer;
}
Вказівники на масиви

Ви також можете створити вказівники на масиви. Це робиться наступним чином:

int *pArray;
pArray = new int[6];


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

int *pArray;
int MyArray[6];
pArray = &MyArray[0];


Зверніть увагу, що замість написання &МуАггау[0], ви можете просто написати МуАггау. Це, звичайно ж, застосовується лише до масивів по причині їх реалізації в мовах C/C++. За загальними правилами необхідно було б написати pArray = &MyArray;, але це неправильно. Якщо ви так напишете то отримаєте вказівник на вказівник на масив (не помилка), що безумовно не те що вам потрібно.

Використання вказівників на масиви

Якщо у вас є вказівник на масив, як його використовувати? Наприклад у вас є вказівник на масив цілих чисел (int Array[]). Вказівник спочатку буде вказувати на перше значення в масиві як показує наступний приклад:
#include <stdio.h>

void main()
{
  int Array[3];

  Array[0] = 10;
  Array[1] = 20;
  Array[2] = 30;

  int *pArray;
  pArray = &Array[0];

  printf("pArray вказує на значення: %d\n", *pArray);
}
Для того щоб перемістити вказівник до наступного елементу масиву, ми можемо написати pArray++. Ми також можемо, як деякі з вас могли вже догадатьтся, панисати pArray + 2, що перемістить вказівник відразу на 2 елементи. Потрібно бути обережним з верхньою межею масиву (в даному випадку це 3 елементи), тому що компілятор не може перевірити чи вийшли ви за межі масиву, використовуючи вказівники. Ви легко можете отримати повний збій системи якщо будете не акуратні. Ось ще один приклад. На цей раз він показує три значення які ми встановили:
#include <stdio.h>
void main()
{
  int Array[3];

  Array[0] = 10;
  Array[1] = 20;
  Array[2] = 30;

  int *pArray;
  pArray = &Array[0];

  printf("pArray вказує на значення: %d\n", *pArray);
  pArray++;

  printf("pArray вказує на значення: %d\n", *pArray);
  pArray++;

  printf("pArray вказує на значення: %d\n", *pArray);
}
Ми також можемо рухати вказівник в будь-яку сторону, так pArray - 2 це 2 елементи від того місця куди вказує вказівник. Переконайтесь, що ви збільшуєте та зменшуєте значення вказівника, а не значення змінної, на яку він вказує. Даний метод використання вказівників і масивів найбільш корисний при використанні циклів, таких як for чи while.

Відмітимо також , що якщо ми маємо вказівник на змінну, наприклад int* pNumberSet, ми можемо розглядати його як масив. Наприклад, pNumberSet[0] еквівалентне *pNumberSet; а pNumberSet[1] еквівалентне *(pNumberSet + 1).

І останнє попередження щодо масивів: якщо ви виділили пам’ять для масиву з використанням оператору new, як показано в наступному прикладі:

int *pArray;
pArray = new int[6];


... то повинні потім видалити його з допомогою оператора:

delete[] pArray;


Зверніть увагу на [] після delete. Це повідомляє компілятор про те, що видаляється цілий масив, а не лише один елемент. Ви повинні використовувати цей метод коли мова йде про масив. В результаті ви отримаєте звільнення пам’яті.

Останнє слово

Останнє зауваження: ви не повинні звільняти пам’ять, якщо вона виділялась не через new, як показано в наступному прикладі:
void main()
{
  int number;
  int *pNumber = number;

  delete pNumber; // помилка - *pNumber не було створено з використанням оператору new.
}
Загальні питання та відповіді

Питання: Чому я отримую помилку "невизначений символ" (symbol undefined) в операторах new і delete?
Відповідь: найімовірніше, це відбувається тому, що ваш вихідний код інтерпретується компілятором як написаний на мові Cі. Оператори new та delete є новими можливостями мови C++. Ситуацію зазвичай можна виправити, вказавши для вихідних файлів розширення *.cpp замість *.c

Питання: Яка різниця між new і malloc?
Відповідь: Оператор new існує тільки в мові C++ і є стандартним (за винятком особливих функцій у Windows) способом виділення пам'яті. Ви не повинні використовувати оператор malloc в C++, хіба що в разі крайньої необхідності. Оскільки malloc не був розроблений для об'єктно орієнтованих можливостей мови C++, його використання для виділення пам'яті для класів призведе до того що конструктор класу викликаний не буде, як приклад того, які проблеми можуть виникнути. У результаті проблем, які виникли при використанні malloc і free, а також тому що вони застаріли для будь-якого використання (в мові C++), в цій статті вони не обговорюються детально. Я не схвалюю використання цих функцій.

Питання: Чи можу я використовувати free і delete разом?
Відповідь: Ви повинні звільнити пам'ять еквівалентною процедурою до тієї, якою ви виділили пам'ять. Наприклад, використовувати free для роботи з пам’яттю, що виділена через malloc, і delete дя пам'яті, яка виділена з допомогою new і так далі.

Посилання

Посилання знаходяться поза темою цієї статті. Але оскільки мене дуже часто запитували про посилання люди які читали цю статтю, я опишу їх коротко. Посилання дуже схожі з вказівниками й у більшості випадків вони можуть бути використані як альтернатива вказівникам. Як ви пам'ятаєте я вже писав, що амперсанд (&) читається при оголошенні як "адреса змінної...". У разі присутності амперсанда в оголошенні в тому вигляді який показаний нижче - його слід читати як "посилання на змінну...".

int& Number = myOtherNumber;
Number = 25;


Посилання це те ж саме що і вказівник на myOtherNumber, за виключенням того, що посилання автоматично розіменовується. Таким чином посилання веде себе як змінна зі значенням а не як змінна з адресою (як індикатор). Ідентичний за змістом код, використовуючи вказівники показаний нижче:

int* pNumber = &myOtherNumber;
*pNumber = 25;


Інша відмінність між вказівниками та посиланнями це те, що ви не можете "скинути" посилання. Це означає що не можна змінювати значення адреси посилання після того, як вона була ініціалізована. Наприклад, код нижче виведе "20":

int myFirstNumber = 25;
int mySecondNumber = 20;
int &myReference = myFirstNumber;
myReference = mySecondNumber;
printf("%d", myFristNumber);

Якщо посилання використати в класі, то значення посилання повинне бути встановлено за допомогою конструктора в такий спосіб:
CMyClass::CMyClass(int &variable) : m_MyReferenceInCMyClass(variable)
{
  // тут код конструктора
}
Висновки

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

- Вказівники являють собою змінні, що вказують на область у пам'яті. Ви визначаєте вказівник, додавши зірочку (*) перед ім'ям змінної (тобто int * number).
- Ви можете отримати адресу будь-якої змінної додавши перед нею & (амперсант), тобто:

pNumber = &my_number;

- Зірочка при оголшенні змінної (наприклад int * number), читається як "адреса пам’яті, на яку вказує...".
- Амперсанд при оголошенні змінної (наприклад int &number), читається як "адреса ..."
- Ви можете виділити пам’ять за допомогою ключового слова new.
- Вказівники ПОВИННІ бути того ж типу, що і змінні, на які ви хочете, щоб вони вказували, тому int *number не буде вказувати на MyClass.
- Ви можете передати вказівники в функцію.
- Ви повинні звільняти пам’ять, яку ви виділили, використовуючи ключове слово delete.
- Ви можете отримати вказівник на масив, якщо він вже існує, використавши &array[0];.
- Ви повинні видалити масив, який був виділений динамічно використавши delete[], а не просто delete.
- Це не абсолютно повне керівицтво по работі з вказівниками. Є ще багато речей, які я міг би розповісти більш детально, такі як вказівники на вказівники, і теми, яких я не торкнувся взагалі, наприклад, вказівники на функції, які занадто важкі для цієї статті. Також є речі, які використовуються дуже рідко, і для початківців буде краще, щоб вони не збивали з пантелику великою кількістю деталей.

От і все! Спробуйте запустити деякі програми, представлені тут і придумайте декілька власних прикладів.

-------------------------------------------------------------------------------
http://translated.by/you/a-beginner-s-guide-to-pointers/into-uk/trans/
© Andrew Peace
Оригінал (англійський): A Beginner’s Guide to Pointers (http://www.codeproject.com/KB/cpp/pointers.aspx)
Переклад: © eReS, ALEXfanat, igorko, rom-broiler, chief, pavlo.dudka.
Ліцензія: The Code Project Open License (CPOL)

translated.by переведено гуртом

коментарі:

Євген 10.01.2011 10:09
Лише схвалені спільнотою публікації можна редагувати чи доповнювати
обмеження на кожному кроці :(
Enetri 10.01.2011 10:44
Так, наявність цифрової дискримінації на Енетрі жодним чином не приховується:
У зв’язку з браком ефективних телепатичних механізмів всі новоенетріанці піддаються несправедливій (та вимушеній) процедурі Цифрової Дискримінації.

Проте, по мірі зміни вашого рейтингу з нейтрального у позитивний (чи протилежний) бік, вся ця "сувора дискримінація" значно і автоматично послаблюється.
Але є й інша, вже хороша новина: редагування ще не проголосованих публікацій можливе при рейтингу у 25+ балів. Вам вже зовсім не багато залишилося, щоб це очевидно несправедливе обмеження було скасоване.
Євген 10.01.2011 10:55
як все складно :)
+1Enetri 10.01.2011 11:33
Певною мірою - так, звичайно. Проте лише для новачків.

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

Якщо маєте ідеї щодо вдосконалення чи спрощення системи - тоді сміло відкривайте окремого публічного обговорення. Виважені та перспективні пропозиції лише вітаються. Зрештою, чинна система теж колись була не зовсім такою, якою вона є зараз - і це все завдячуючи колективним зусиллям всієї спільноти проекту Енетрі.
misha.arp 10.10.2011 05:28
Як на мене є одна помилка в розділі "Для закріплення матеріалу: Приклади".
Приклад коду "Пастка! Спробуйте знайти помилку в наведеній нижче програмі:", який по задумці НЕ повинен був присвоювати вказівнику *pPointer значення 25, насправді присвоює його. І програма виводить на монітор фразу "Значення *pPointer: 25". Оскільки тут *pPointer оголошено як глобальна змінна, і тому всі зміни, що відбулися із вказівником всередині функції, збережуться після виходу з неї.

додати коментар: