Вказівник - це змінна або стала, значенням якої є адреса іншої змінної або сталої. Вказівники - потужна і потенційно небезпечна можливість мови C. Зрозуміти вказівники буває непросто.
int i = 5;
Присвоївши змінній значення, ми можемо поставити до неї два запитання.
Як її звати?
Її звати i.
Що в ній зберігається?
У ній ціле число 5.
Можна поставити ще й третє запитання.
Де вона?
Вона розташована за адресою 0x6dfed4.
Стоп, що?
Змінні та сталі зберігають своє значення в ділянці памʼяті. Цю ділянку памʼяті називають адресою змінної або сталої.
Щоб отримати адресу змінної, ми використовуємо оператор взяття адреси, тобто &.
int i = 5;
int *i_address = &i;
Стоп, а що означає той * перед i_address? Хіба це не множення?
Щоб оголосити вказівник на змінну певного типу, записують тип, за ним *, а далі ідентифікатор.
Цей * називають оператором непрямого доступу, бо він непрямо вказує на значення, адресу якого зберігає вказівник.
Гаразд, але навіщо створювати змінну лише для того, щоб зберігати адресу іншої змінної?
Чому нам взагалі важливо знати адресу змінної?
Річ у тім, що коли змінні лежать у масиві, нам важливо знати, де саме вони розташовані.
Коли ми створюємо масив ось так
int my_ints[] = {1, 2};
Значення my_ints насправді є його адресою, яка до того ж збігається з адресою його першого елемента.
Адреса його другого елемента дорівнює адресі першого елемента плюс розмір int у цій системі.
Щоб краще це побачити, спробуймо виконати такий код
#include <stdio.h>
int main() {
// my_int's value is its address
int my_ints[] = {1, 2};
// since my_ints is already an address
// we don't need the address operator to make a pointer from it.
// a pointer to my_ints holds the address of my_ints
int *my_ptr = my_ints;
// the address of my_ints and the value of my_ptr are the same
printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
// the address of the first element is the same as the values in the line above
printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
// the address of the second element has increased by the size of an int
printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); //
return 0;
}
Попередній приклад показує, чому індексація масиву починається з 0, а не з 1.
Індекс елемента масиву множать на розмір типу й додають до початкової адреси масиву.
Оскільки перший елемент збігається з початком масиву, щоб отримати перший елемент, розмір типу множать на 0 і додають до початкової адреси.
Щоб отримати другий елемент, розмір типу множать на 1 і додають до початкової адреси.
Якщо тип завдовжки чотири байти, адреса другого елемента буде на чотири байти вищою, ніж адреса першого.
Саме тому вказівники оголошують із зазначенням певного типу.
Якби вказівник не знав, на який тип він указує, він не знав би, на скільки байтів просунутися, коли до нього додають 1.
Додавання 1 до вказівника не обовʼязково означає, що ми додаємо до адреси один байт.
Це означає, що ми додаємо 1, помножену на довжину його типу в байтах.
Але це ще не все! Невже хтось думав, що ми вже закінчили з *?
Тепер, коли ми розуміємо, як адреси працюють з індексацією масиву, який сенс, якщо ми не отримуємо значення, збережене за адресою?
Як це зробити?
Щоб отримати значення, збережене за адресою, ми використовуємо оператор непрямого доступу, який, як ми вже згадували, - це *.
У цьому випадку оператор непрямого доступу іноді називають також оператором розіменування, бо він розіменовує значення, адресу якого має вказівник.
Назва «оператор розіменування» під час розіменування відрізняє його від оператора непрямого доступу, який використовують для оголошення вказівника, але формально * в обох випадках називають оператором непрямого доступу.
Та годі!
Серйозно?
Скільки ж усього застосувань у *?!
Множення, оголошення вказівника, а тепер ще й розіменування!
Це заплутано!
І що взагалі таке розіменування?
Погляньмо на приклад використання оператора непрямого доступу для розіменування
#include <stdio.h>
int main() {
// define the variable
int my_ints[] = {1, 2};
// define the pointer from the address of the variable
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
// prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
return 0;
}
Дужки допомагають прибрати двозначність намірів під час арифметики вказівників. Наступний приклад може надрукувати не те, що мав на увазі програміст
#include <stdio.h>
int main() {
int my_ints[] = {1, 42};
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *++my_ptr is 42.
printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
// prints: my_ints[1] is 43. ++*my_ptr++ is 43.
printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);
return 0;
}
І *, і ++ мають однаковий пріоритет, але обчислюються справа наліво.
У першому рядку my_ptr спершу інкрементують до другого елемента, який потім розіменовують.
У другому рядку my_ptr, який у першому рядку було інкрементовано до другого елемента, спершу розіменовують, а потім розіменоване значення інкрементують.
Якщо додати цей рядок до попереднього прикладу
// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);
У цьому рядку ми інкрементували вказівник так, що він вийшов за межі масиву.
Ми вийшли за межі масиву й указуємо на наступні чотири байти в памʼяті, які вказівник інтерпретує як int, але насправді ці байти можуть належати іншій змінній чи змінним.
Ці чотири байти можуть бути частиною double або чотирма char.
Проблема в тому, що ми взагалі не повинні дивитися на них своїм вказівником.
Можна було б використати
// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);
Оскільки індексація масиву реалізована через вказівник, тут діє та сама небезпека виходу за межі масиву.
А оскільки рядок тексту (англ. string) - це масив char, вказівники можна використовувати і з рядками тексту.
Рядок тексту часто оголошують із синтаксисом вказівника, ось так
char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";
Але є одне питання. Чому ми ставимо зірочку поруч з ідентифікатором? Чому не поруч із типом, адже це вказівник на цей тип? Бо ось це
int* my_ptr1, my_ptr2_that_is_really_an_int;
оголошує не два вказівники на int, а вказівник на int та int.
Щоб оголосити два вказівники на int, треба написати ось так
int *my_ptr1, *my_ptr2;
Тож краще ставити * ближче до ідентифікатора, щоб зменшити двозначність.
Або оголошувати лише один ідентифікатор у рядку.
А якщо комусь усе ще мало *, ласкаво просимо до...
Масиви рядків тексту часто записують ось так
#include <stdio.h>
void print_strings(char **passed_in_strings) {
printf("%s\n", *passed_in_strings);
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char *my_strings[] = {"One string", "Two string"};
print_strings(my_strings);
return 0;
}
Це може бути неочевидним, але масив рядків тексту оголошують та ініціалізують як вказівник на масив масивів char.
Коли масив масивів char передають у print_strings(), сигнатура описує його як вказівник на вказівник(и) на char.
То як нам дізнатися, що char ** - це справді вказівник на масив вказівників на масиви char, а не вказівник на вказівник на єдиний char?
Ніяк.
Це одна з багатьох речей, які роблять роботу з вказівниками заплутаною. Наступний код може скомпілюватися
#include <stdio.h>
void print_strings(char **passed_in_strings) {
// prints: A (followed by gibberish)
printf("%s\n", *passed_in_strings);
// prints gibberish
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char my_char = 'A';
char *my_ptr = &my_char;
char **my_ptr_to_ptr = &my_ptr;
print_strings(my_ptr_to_ptr);
return 0;
}
Але print_strings() використовують не так, як задумано.
Інша сигнатура не допоможе. Якщо змінити сигнатуру на таку
void print_strings(char *passed_in_strings[]) {
// code snipped
}
код усе одно може скомпілюватися й так само надрукувати абракадабру.
Але насправді немає сенсу передавати вказівник на вказівник на єдиний char.
Найімовірніше, коли ми бачимо char **, це вказівник на масив вказівників на масиви char.
Ми вже бачили, як вказівник передають у функцію. Але навіщо взагалі передавати вказівник у функцію?
У мові C аргументи передають у функції за значенням. Це означає, що значення копіюють з аргументу, і ця копія є локальною для функції. Коли функція завершується, скопійоване значення теж припиняє існувати.
Погляньмо, що станеться, коли ми спробуємо змінити значення всередині функції
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
//prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", a, b);
}
int main() {
int a = 3, b = 4;
swap(a, b);
// prints: After 'swap', a is 3 and b is 4
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
Це тому, що хоч змінні й мають однакові назви, a та b у swap() - це не ті самі a та b, що в main().
Вони - копії значень a та b.
Чи можемо ми змінити swap, щоб вона працювала як задумано?
Так, можемо!
Ми можемо передати адреси a та b як вказівники.
Але який із цього сенс, якщо функція копіює вказівники?
Сенс є, бо скопійована адреса - це все та сама адреса.
Розіменувавши адресу, ми отримуємо доступ до початкового значення таким, яким воно було визначене в main().
#include <stdio.h>
// pass in pointers to a and b
void swap(int *a, int *b) {
// set temp to value at a's dereferenced address
int temp = *a;
// set a's value to value at b's dereferenced address
*a = *b;
// set b's value to temp
*b = temp;
// prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}
int main() {
int a = 3, b = 4;
swap(&a, &b);
// prints: After 'swap', a is 4 and b is 3.
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
Тепер у swap багато *, але вона нарешті працює як задумано.
Оператор непрямого доступу можна читати як «значення за адресою».
Отже, *a - це значення за адресою a.
Наскільки добре це працює з рядками тексту?
Ми знаємо, що значення змінної-масиву - це адреса, з якої починається масив.
І ми знаємо, що рядок тексту можна визначити або як масив char, char my_string[], або як вказівник на char, char *.
Чим вони відрізняються?
У цьому коді ми передаємо аргумент char * у describe_me().
Аргумент adjective визначено як масив char, і функція приймає його як вказівник на char.
#include <stdio.h>
#include <string.h>
// takes pointer to char
void describe_me(char *adjective){
strcpy(adjective, "evil");
// prints: I am evil.
printf("I am %s.\n", adjective);
}
int main() {
// defined as a char array
char adjective[] = "good";
describe_me(adjective);
// prints: I am really evil.
printf("I am really %s.\n", adjective);
return 0;
}
Нам вдається внести в adjective зміну, яка зберігається, що показує: визначений масив char можна прийняти як вказівник на char.
А тепер спробуймо визначити adjective безпосередньо як вказівник на char.
#include <stdio.h>
#include <string.h>
void describe_me(char *adjective){
// segmentation fault on this line when program is executed
strcpy(adjective, "evil");
printf("I am %s.\n", adjective);
}
int main() {
char *adjective = "good";
describe_me(adjective);
printf("I am really %s.\n", adjective);
return 0;
}
Памʼятаємо, що вказівники потужні, але й потенційно небезпечні?
Але чому це не працює?
Масив - це вказівник, і з масивом char вказівник працював!
Причина в тому, що раніше adjective було визначено як масив char, який є змінною, що належить main().
А тепер adjective визначено як літерал рядка тексту, тобто chars записуються в статичну памʼять лише для читання, яка належить усій програмі.
І те, і те - вказівники, але вони вказують на різні речі.
Спроба змінити літеральне значення рядка тексту - це як спроба змінити літеральне значення 5.
Це не дозволено.
Тож проблема не в тому, що це вказівник.
Проблема в тому, на що він указує.